A Python toolkit for Ghanaian phone numbers, cedi amounts and Mobile Money transactions
📦 Python library · 💻 Command line · 🪟 Desktop app · 🌐 Web app
📚 Documentation · 🖥️ Apps · ⬇️ Download for Windows · 🚀 Quickstart · 🏗️ Architecture · 💻 CLI · 🤝 Contributing
The cedikit desktop app flagging a fake MoMo payment message
📖 Table of contents
- cedikit in plain words ← start here if you're not a programmer
- Desktop and web apps ← use cedikit without writing code
- Feature tour: try these tests yourself ← see every feature working
- Why cedikit?
- Features
- Installation
- Quickstart
- Command line
- Architecture
- How it works
- Project structure
- Data model
- Design principles
- Quality and testing
- Privacy and ethics
- Limitations
- Roadmap
- Contributing
- Licence and acknowledgements
🧒 cedikit in plain words
No computer knowledge needed for this part.
🛒 Meet Auntie Akosua
Auntie Akosua sells provisions in Kumasi. Most of her customers pay with Mobile Money (MoMo). Every time someone pays, her phone gets a text message like this:
Payment received for GHS 50.00 from KOFI MENSAH. Current Balance: GHS 320.00 ...
By the end of the month she has hundreds of these messages, and four problems:
- 😵 She can't see the big picture. How much did she make this week? Who are her best customers? The answers are buried in hundreds of texts.
- 📓 Her records are messy. Customers' phone numbers are written in many different ways
(
024 412 3456,+233244123456,244123456…), so the same person looks like three people. - 🧮 Small mistakes add up. Computers are surprisingly bad at adding money with pesewas: ask one to add GHS 0.10 and GHS 0.20 and you can get GHS 0.30000000000000004. Over thousands of sales, the totals drift.
- 🦹 Tricksters send fake messages. A thief shows her a text that looks like a real payment, takes the goods, and walks away. She was never paid.
cedikit is a set of careful helpers that fixes all four.
🧰 The helpers inside cedikit
| Helper | It works like… | What it does for Auntie Akosua |
|---|---|---|
| 📱 Number tidier | a teacher who makes everyone write their name the same way | Rewrites every phone number in one standard form, and says which network it most likely belongs to (MTN, Telecel or AT) |
| 💰 Money counter | a shopkeeper who never loses a single pesewa | Adds, rounds and writes money exactly, even in words: "Forty-five Ghana cedis and fifty pesewas" |
| 📩 Message reader | a secretary who reads every MoMo text for you | Picks out who paid, how much, when, the fee and the new balance, and writes it down neatly |
| 📷 Screenshot reader | someone reading a letter out loud to you | Reads the message straight off a screenshot, so you don't have to type or copy anything |
| 📒 Account book | an accountant | Adds everything up: money in, money out, fees, best customers, weekly totals. Then makes an Excel file and charts |
| 🚨 Scam detector | a wise security guard | Looks at a payment message and says "looks safe" ✅ or "be careful" ⚠️, and explains why |
| 🧾 Fee calculator | a friend who knows the price list | Estimates how much the network will charge for sending or withdrawing money |
| 🪪 ID checker | someone checking a form is filled in correctly | Checks that a Ghana Card number or GhanaPost digital address is written correctly |
🔄 What happens, step by step
flowchart LR
A["📩 MoMo text<br/>messages"] --> B["🔍 cedikit<br/>reads them"]
B --> C["📋 Neat list of<br/>every payment"]
C --> D["📊 Totals, charts<br/>and an Excel file"]
B --> E["🚨 Scam check"]
E --> F["✅ Looks safe"]
E --> G["⚠️ Be careful:<br/>here's why"]
- You give cedikit the messages: copy them from the phone, take a screenshot, or an app does it for you.
- It reads each one and turns it into a neat line: "50 cedis, from Kofi Mensah, Monday 10:15am, balance 320 cedis."
- It adds everything up into a monthly summary, just like an accountant's report.
- It checks suspicious messages for signs of a trick.
🕵️ How the scam detector thinks
It works like a detective looking for clues. One clue might be an accident; several clues together mean real danger.
| 🔎 Clue | Why it matters |
|---|---|
| Who sent it? | Real MoMo alerts come from "MobileMoney" or "T-CASH". A message from an ordinary phone number like 055 123 4567 is almost always fake. |
| Does it look exactly like a real alert? | Fakes copy the wording but get it slightly wrong, e.g. "Cash In for" instead of "Cash In received for". |
| Spelling mistakes? | Real alerts are written by a computer and never have typos. "Avaliable balan" is a giveaway. |
| Scary or pushy words? | "Your account is blocked, don't try your PIN" or "I sent it by mistake, send it back" are classic tricks. |
| Disguised letters? | Tricksters write "Suspéndéd" with strange accents to sneak past phone spam filters. |
| Does the maths work? | If yesterday's balance was 100 cedis and you "received" 50, today's balance must be 150. If the message says 700, something is wrong. |
The more clues it finds, the louder the alarm: LOW 🟢, MEDIUM 🟡 or HIGH 🔴. It always explains which clues it found.
🔒 Is my information safe?
Yes. cedikit works entirely on your own computer or phone app. It never sends your messages, numbers or money details anywhere on the internet.
👥 Who is it for?
- 🏪 Shop owners can use the desktop app directly: no coding, just buttons (see below).
- 👩💻 Programmers use it like ready-made building blocks to build apps for Ghanaian businesses, so they don't have to build these helpers from scratch.
- 🏪 Traders also benefit through apps other people build on cedikit: cleaner records, automatic accounts and scam warnings.
- 🎓 Students and researchers use it to study Mobile Money data.
📚 Words you might see
| Word | Meaning |
|---|---|
| Mobile Money (MoMo) | Sending and receiving money using a phone, e.g. MTN MoMo or Telecel Cash |
| SMS | A text message |
| Library | A box of ready-made tools that programmers put inside their own apps |
| Python | A popular programming language; cedikit is written in it |
| Open source | The code is free for anyone to see, use and improve |
| PyPI | The online "app store" for Python tools; cedikit lives there |
| Offline | Works without the internet |
🖥️ Desktop and web apps
Since version 1.1.0 (on PyPI from 1.2.0). cedikit also comes as software with windows and buttons, so anyone can use it without writing code. Both apps have the same six tabs and give exactly the same answers.
| Tab | What you do |
|---|---|
| ⚠️ Check a message | Paste a payment SMS or open a screenshot of it → LOW / MEDIUM / HIGH risk, with reasons |
| 📒 Account book | Paste MoMo messages, open a file or open screenshots → totals, a table of every payment, Save as Excel |
| 📱 Phone numbers | Paste a list or open a customer CSV → cleaned numbers, networks, bad numbers flagged |
| 💰 Money & fees | Amounts in words; estimate MoMo charges |
| 🪪 Ghana Card & address | Check a Ghana Card number or GhanaPostGPS address is written correctly |
| 📘 About | What cedikit does, and the safety reminder |
Every tab has a "Try with samples" button, so you can see it working straight away.
📷 Just take a screenshot
Most people have the message as a screenshot, not as text. Open the screenshot and cedikit reads the message off the picture, fills in who sent it (from the name or number at the top of the chat), and checks it. If the picture shows several messages, pick the one you want. The Account book tab can read many screenshots at once.
A screenshot read: two messages found, sender filled in automatically, verdict shown
Reading pictures happens on your own computer (Windows' built-in text recognition, or RapidOCR on Mac and Linux). Nothing is uploaded. It's good but not perfect, so always compare the text with your screenshot.
🧭 Feature tour: try these tests yourself
Every test below uses made-up data and works offline. Click a feature to open it. The same
results are checked automatically by cedikit's test suite (tests/test_app.py) and by the
app's built-in self-test (cedikit-app --selftest), so this tour stays accurate.
⚠️ Spot a fake payment alert
Try this
- Open the Check a message tab.
- In Try an example..., choose Fake cash-in. (It fills in the message and the sender
+233591234567.)
You'll see: 🔴 HIGH RISK: Very likely fake (score 0.96), because it came from a personal phone number, doesn't match any genuine MTN format, and has spelling mistakes (Avaliable, balan).
From the command line: cedikit fraud check "Cash In for GHS150.00 from ... Avaliable balan 640.35" --sender +233591234567
🚫 Spot the 'your account is blocked' trick
Try this
- In Check a message, choose the example Fake 'account blocked'.
You'll see: 🔴 HIGH RISK (score 0.98): personal sender, it tells you what to do with your PIN, and it claims your account is blocked (so you won't check your real balance).
From the command line: cedikit fraud check "SORRY YOU HAVE BEING BLOCKED ... DO NOT TRY YOUR PIN" --sender +233591234567
✅ See a genuine alert pass
Try this
- In Check a message, choose the example Genuine MTN payment (sender
MobileMoney).
You'll see: 🟢 LOW RISK: Looks safe (score 0.00), with no warning signs, plus the reminder to still confirm in your MoMo app.
From the command line: cedikit fraud check "Payment received for GHS 50.00 from KOFI MENSAH ..." --sender MobileMoney
📷 Check a screenshot instead of typing
Try this
- In Check a message, click Try a sample screenshot (or Open screenshot... for your own).
You'll see: Read 2 messages, the sender MobileMoney filled in automatically from the top of the chat, the newest message checked (🟢 LOW), and a picker to check the other message.
From the command line: python -c "from cedikit import ocr; print(ocr.read_screenshot('shot.png'))"
📒 Turn MoMo messages into an account book
Try this
- Open the Account book tab.
- Click Try with sample messages.
- Click Save as Excel... to get a workbook with Transactions, Summary, Cash flow and Categories sheets.
You'll see: 6 transactions: money in GH₵ 245.00, money out GH₵ 350.00, fees GH₵ 1.00, last MTN balance GH₵ 94.00, and each payment categorised (sales, supplies, cash withdrawal, loan repayment).
From the command line: cedikit sms parse inbox.txt --sender MobileMoney --export xlsx
📱 Clean up customers' phone numbers
Try this
- Open the Phone numbers tab and click Try with samples (or Open CSV... for your customer list).
- Click Save cleaned list... to download the result.
You'll see: 7 numbers: 0 valid, 5 fixed, 2 invalid. Every number is rewritten as +233... with its likely network (MTN, Telecel, AT). 12345 (too short) and 021 123 4567 (a landline) are shown in red with the reason.
From the command line: cedikit phone clean customers.csv --column phone
💰 Amounts in words and MoMo charges
Try this
- Open Money & fees, type
1250.50and click Show. - Under Estimate MoMo charges, keep MTN, Cash out (withdraw),
500, and click Estimate.
You'll see: GH₵ 1,250.50 and One thousand two hundred and fifty Ghana cedis and fifty pesewas; then a fee of GH₵ 5.00, E-Levy GH₵ 0.00, and where those numbers come from.
From the command line: cedikit money words 1250.50 · cedikit fees estimate MTN cash_out 500
🪪 Check a Ghana Card number
Try this
- Open Ghana Card & address, type
gha 123456789 0(any spacing or case) and click Check.
You'll see: ✔ GHA-123456789-0 is correctly written (citizen card), plus a masked copy for sharing: GHA-12*****89-0.
From the command line: cedikit ids check "gha 123456789 0"
🌍 Check a foreign national's Ghana Card
Try this
- Type
FGN-987654321-5and click Check.
You'll see: ✔ FGN-987654321-5 is correctly written (foreign national card). Cards for non-citizens start with FGN.
From the command line: cedikit ids check FGN-987654321-5
📍 Check a GhanaPostGPS digital address
Try this
- Type
ak0395028and click Check.
You'll see: ✔ AK-039-5028 is correctly written: Kumasi Metropolitan, Ashanti, with the hyphens added and the district and region looked up.
From the command line: cedikit ids check ak0395028
❌ See what a wrong ID looks like
Try this
- Type
GHA-12345-6(too few digits) and click Check.
You'll see: ✖ Not a correctly written Ghana Card number or GhanaPostGPS address, with examples of the right format. (These are format checks only: they never confirm that a card or address really exists.)
From the command line: cedikit ids check GHA-12345-6
🪟 Desktop app
A normal Windows program. Pick one way to start it:
| How | Steps |
|---|---|
| Stand-alone program (no Python needed) | Download cedikit-app.exe from the Releases page and double-click it |
| With Python | pip install "cedikit[app]", then run cedikit app (or cedikit-app) |
🌐 Web app
The same tabs in your web browser:
pip install "cedikit[web]"
cedikit web # opens http://localhost:8501
It runs only on your own computer (localhost). The launcher also switches off Streamlit's
anonymous usage statistics, so nothing is sent online.
🧑💻 How the apps are built
flowchart LR
D["🪟 Desktop app<br/>Tkinter · cedikit/app/desktop.py"] --> C["🔗 Shared app logic<br/>cedikit/app/common.py<br/>labels · tables · examples"]
W["🌐 Web app<br/>Streamlit · cedikit/app/web.py"] --> C
C --> L["📦 cedikit library<br/>fraud · ledger · phone · money · fees · ids"]
E["📦 cedikit-app.exe<br/>PyInstaller · packaging/build_exe.py"] -.bundles.-> D
Both apps only handle screens and buttons. All the logic lives in the library and in
cedikit/app/common.py, which is why they always agree. The .exe bundles Python, the desktop
app and cedikit's data files into one 14 MB program; build it with
python packaging/build_exe.py, which also self-tests the result.
💡 Why cedikit?
Mobile Money is central to everyday commerce in Ghana, yet every developer building a local product ends up solving the same five problems again:
| # | Problem | What goes wrong | cedikit's answer |
|---|---|---|---|
| 1 | 📱 Messy phone numbers | 0244123456, +233 24 412 3456, 233244123456, 24 412 3456… each app writes its own buggy cleaner |
One normaliser for every common format, with reasons for rejections |
| 2 | 💸 Unsafe money handling | Floats turn 0.1 + 0.2 into 0.30000000000000004 |
Decimal everywhere; floats are refused |
| 3 | 📩 Unstructured MoMo records | Small businesses keep their books in free-text SMS | Parser turns SMS into structured transactions and ledgers |
| 4 | 🚨 Fake payment alerts | Traders release goods on a fake "you have received GHS X" message | Explained fraud checks: sender, wording, balance maths and more |
| 5 | 🌍 No Ghana-focused toolkit | Global libraries know nothing about MoMo, cedis or local scams | Built from real (anonymised) Ghanaian messages |
✨ Features
📦 Installation
Pick what suits you:
| I am… | Get this | How |
|---|---|---|
| 🏪 Not a programmer | 🪟 Desktop app | Download cedikit-app.exe and double-click it. No Python needed. |
| 🙋 Comfortable with Python | 🪟 Desktop or 🌐 web app | pip install "cedikit[app]" → cedikit app, or pip install "cedikit[web]" → cedikit web |
| 👩💻 A developer | 📦 Library + 💻 CLI | pip install cedikit (below) |
pip install cedikit # core library + the `cedikit` command
pip install "cedikit[all]" # + every optional extra below
Requires Python 3.10+. The core depends only on PyYAML and Typer.
| Extra | Adds | Enables |
|---|---|---|
excel |
openpyxl | ledger.export("file.xlsx") |
charts |
matplotlib | ledger.plot() |
pandas |
pandas | df["phone"].cedikit.normalise(), ledger.to_dataframe() |
ml |
scikit-learn, joblib | fraud.classifier.ScamClassifier |
pydantic |
pydantic | GhanaPhone, CediAmount field types |
django · flask |
django · wtforms | Form and model validators |
ocr |
Pillow + Windows OCR / RapidOCR | ocr.read_screenshot("shot.png"): messages from screenshots |
app |
openpyxl, ocr | The desktop app with Excel export and screenshots (cedikit app) |
web |
streamlit, openpyxl, ocr | The web app (cedikit web) |
🚀 Quickstart
📱 Phone numbers
from cedikit import phone
phone.normalise("024 412 3456") # '+233244123456'
phone.format("+233244123456", "pretty") # '024 412 3456'
phone.likely_network("0244123456").network # 'MTN' (likely - numbers can be ported)
phone.mask("0244123456") # '024****456'
report = phone.clean_column(["0244123456", "+233 50 123 4567", "12345"])
print(report) # 3 numbers: 0 valid, 2 fixed, 1 invalid
💰 Money
from cedikit import money, Cedi
money.parse("GH₵1.2k") # Decimal('1200.00')
money.format("1200.5") # 'GH₵ 1,200.50'
money.to_words("1200.50") # 'One thousand two hundred Ghana cedis and fifty pesewas'
sum([Cedi("0.10"), Cedi("0.20")]) # Cedi('0.30') - exact, unlike 0.1 + 0.2
money.format(1200.5) # CediTypeError: floats can't represent pesewas exactly
📩 SMS → 📒 ledger
from cedikit import sms
from cedikit.ledger import Ledger
result = sms.parse(message_text, sender="MobileMoney")
if result.ok:
tx = result.transaction
print(tx.type, tx.amount, tx.counterparty.name, tx.balance, tx.confidence)
ledger = Ledger.from_messages(inbox, sender="MobileMoney").categorise()
print(ledger.summary())
ledger.cash_flow("week") # money in / out per week
ledger.top_counterparties(5, by="value") # biggest customers and suppliers
ledger.balance_gaps() # where a message is probably missing
ledger.export("september.xlsx") # Transactions, Summary, Cash flow, Categories
🚨 Fake-alert check
from cedikit import fraud
report = fraud.check(suspicious_text, sender="+233591234567", history=ledger.transactions)
print(report)
Risk: HIGH (score 0.99)
Reasons:
- Sent from a personal phone number (+233 59 123 4567), not an official sender ID such as
MobileMoney or T-CASH. Genuine alerts never come from personal numbers.
- Claimed balance GHS 640.35 does not follow from your last genuine balance of GHS 322.10
(expected GHS 472.10), unless you made other transactions in between.
- Looks like a Mobile Money alert but does not match any genuine message format.
- Contains spelling mistakes ('Avaliable', 'balan'). Genuine alerts are machine-generated
and don't have typos.
Before releasing goods or cash, confirm the payment in your official Mobile Money app ...
📷 Screenshots
from cedikit import ocr, fraud
shot = ocr.read_screenshot("whatsapp_image.jpg") # needs: pip install "cedikit[ocr]"
shot.sender # 'MobileMoney' (read from the chat header)
for message in shot.messages: # one entry per chat bubble
print(fraud.check(message, sender=shot.sender).risk)
🧾 Fees and 🪪 IDs
from datetime import date
from cedikit import fees
from cedikit.ids import ghana_card, gpgps
fees.estimate("MTN", "cash_out", "500").fee # Decimal('5.00')
fees.estimate("MTN", "send_other_network", "4000", date(2024, 5, 26)).tax
# Decimal('40.00') - the 1% E-Levy then
ghana_card.card_type("FGN-123456789-0") # 'foreign national'
gpgps.parse("ak0395028").district # 'Kumasi Metropolitan'
💻 Command line
Installing cedikit adds a cedikit command:
cedikit phone clean customers.csv --column phone # clean a CSV column
cedikit sms parse inbox.csv --export xlsx # SMS → Excel ledger
cedikit fraud check "Cash receive for 200.00 ..." --sender 0543268728
cedikit fees estimate TELECEL send_other_network 40
cedikit ids check AK-039-5028
| Command | Purpose |
|---|---|
phone clean · phone check |
Normalise a CSV column (adds status and reason columns) · inspect one number |
money parse · money words |
GH₵1.2k → 1200.00 · amount in words |
sms parse · sms anonymise |
Messages → ledger summary, balance gaps, export · anonymise before sharing |
fraud check |
Risk rating with reasons (- reads from stdin) |
fees estimate |
Fee and E-Levy estimate for a date |
ids check |
Ghana Card or GhanaPostGPS format check |
🏗️ Architecture
cedikit is a set of small, independent modules over a data layer of YAML files. Nothing touches the network. Rules that change over time (prefixes, message formats, fees, scam phrases, district codes) live in data, so they can be updated without code changes.
flowchart TB
subgraph Users["👥 Users"]
APP["Python apps, bots, APIs"]
TERM["Terminal users"]
DS["Analysts (pandas, notebooks)"]
end
subgraph Interfaces["🔌 Interfaces"]
API["Public API<br/>cedikit.*"]
CLI["CLI<br/>cedikit (Typer)"]
INT["Integrations<br/>pandas · Pydantic · Django · Flask"]
end
subgraph Core["⚙️ Core modules"]
PHONE["📱 phone"]
MONEY["💰 money"]
SMS["📩 sms<br/>parser · anonymiser"]
OCR["📷 ocr<br/>screenshots"]
FRAUD["🚨 fraud<br/>rules · classifier"]
LEDGER["📒 ledger"]
FEES["🧾 fees"]
IDS["🪪 ids"]
EVAL["📏 evaluation"]
end
subgraph Data["🗂️ Data layer (YAML, versioned)"]
D1["data/prefixes.yaml"]
D2["sms/templates/*.yaml"]
D3["fraud/scam_phrases.yaml"]
D4["fees/tables/*.yaml"]
D5["ids/regions.yaml"]
end
APP --> API
TERM --> CLI
DS --> INT
CLI --> Core
INT --> Core
API --> Core
SMS --> PHONE
OCR --> SMS
SMS --> MONEY
FRAUD --> SMS
FRAUD --> LEDGER
LEDGER --> SMS
FEES --> MONEY
EVAL --> FRAUD
EVAL --> SMS
PHONE -.reads.-> D1
SMS -.reads.-> D2
FRAUD -.reads.-> D3
FEES -.reads.-> D4
IDS -.reads.-> D5
Module dependencies
| Module | Depends on | Data it reads |
|---|---|---|
phone |
— | data/prefixes.yaml |
money |
— | — |
sms |
phone, money |
sms/templates/mtn.yaml, telecel.yaml |
ledger |
sms, money, phone |
— |
fraud |
sms, ledger, phone, money |
fraud/scam_phrases.yaml |
fees |
money |
fees/tables/*.yaml |
ids |
— | ids/regions.yaml |
ocr |
sms (to recognise senders) |
— (uses Windows OCR or RapidOCR) |
evaluation |
sms, fraud |
your labelled YAML files |
⚙️ How it works
📩 SMS parsing pipeline
Each message format is a template: a regular expression written with readable
{{placeholders}}. When a telco changes its wording, a new template is added; the parser
code doesn't change.
flowchart LR
A["Raw SMS<br/>+ sender ID"] --> B["Clean<br/>whitespace, GH₵/GHC → GHS"]
B --> C{"Official sender?<br/>MobileMoney / T-CASH"}
C -- yes --> D["Try that network's<br/>templates first"]
C -- no --> E["Try all templates"]
D --> F["Match template<br/>+ search extras"]
E --> F
F --> G["Validate fields<br/>amount → Decimal<br/>phone → E.164<br/>date → datetime (GMT)"]
G --> H["Confidence =<br/>valid fields ÷ required × weight"]
H --> I{"Any match?"}
I -- yes --> J["✅ ParseResult<br/>status = parsed<br/>best Transaction"]
I -- no --> K["❔ ParseResult<br/>status = unrecognised<br/>(never raises)"]
A template looks like this (from sms/templates/mtn.yaml):
- name: mtn_cash_out
type: CASH_OUT
pattern: >-
Cash Out made for {{amount}} to {{counterparty_name}} ?\.
Current Balance:? {{balance}}
Financial Transaction Id: {{transaction_id}}\.
.*?Fee charged: {{fee}}
fields:
transaction_id: '\d{11}'
Supported formats (12):
| Telecel Cash | MTN MoMo | |
|---|---|---|
| 📤 Send | same network, other network | other network |
| 📥 Receive | same network, other network | payment received |
| 🛒 Pay | — | merchants, loans, auto-debits |
| 🏧 Cash in / out | cash in | cash in, cash out |
| 📶 Airtime / data | airtime purchase, airtime notice | data bundle |
🚨 Fraud scoring
Every check produces independent signals with a strength between 0 and 1. They are combined with a noisy-OR, so separate red flags reinforce each other and passing checks never water down a failing one:
$$\text{score} = 1 - \prod_{i}(1 - s_i) \qquad \text{LOW} < 0.35 \le \text{MEDIUM} \le 0.70 < \text{HIGH}$$
flowchart TB
M["SMS + sender + optional history"] --> P["Parse with sms templates"]
P --> C1["👤 Sender<br/>personal number? 0.85"]
P --> C2["📝 Format<br/>looks like an alert<br/>but matches no template? 0.5"]
P --> C3["🔤 Spelling<br/>'Avaliable', 'balan'… 0.5"]
P --> C4["💬 Scam phrases<br/>PIN, 'account blocked',<br/>'send it back'… 0.3–0.6"]
P --> C5["🎭 Disguised letters<br/>'Suspéndéd' 0.5"]
P --> C6["🔢 Transaction ID<br/>wrong length 0.4"]
P --> C7["⚖️ Balance maths<br/>vs. genuine history 0.7"]
P --> C8["🤖 Optional ML model<br/>probability × 0.6"]
C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8 --> N["Noisy-OR<br/>1 − ∏(1 − sᵢ)"]
N --> R["FraudReport<br/>risk · score · reasons · advice"]
📒 From SMS to insight
flowchart LR
S["📩 SMS inbox<br/>(.txt or .csv)"] --> L["Ledger.from_messages"]
L --> F1["Skip notices<br/>(affects_wallet = false)"]
L --> F2["Drop duplicate<br/>transaction IDs"]
L --> F3["Keep unrecognised<br/>for review"]
F1 & F2 --> T["Transactions"]
T --> A1["summary()"]
T --> A2["cash_flow()"]
T --> A3["top_counterparties()"]
T --> A4["categorise()"]
T --> A5["balance_gaps()"]
A1 & A2 & A4 --> X["📊 Excel · CSV · JSON · charts"]
affects_wallet: some messages repeat a transaction without moving money. Telecel
sends "you have received airtime" after an airtime purchase, with the same transaction ID.
These are recognised but marked affects_wallet=False, so ledgers never count them twice.
Balance gaps: consecutive balances must satisfy
new = previous ± amount − fee − tax. When they don't, a message is usually missing, such
as an automatic loan deduction. Occasionally a message is fake.
📁 Project structure
cedikit/
├── 📂 src/cedikit/
│ ├── __init__.py # public API: phone, money, sms, fraud, ledger, fees, ids
│ ├── phone.py # 📱 normalise, validate, format, mask, clean_column
│ ├── money.py # 💰 Decimal parsing/formatting, to_words, Cedi type
│ ├── ledger.py # 📒 Ledger, summaries, cash flow, exports, charts
│ ├── evaluation.py # 📏 parser accuracy, fraud precision/recall
│ ├── ocr.py # 📷 read messages + sender from screenshots
│ ├── cli.py # 💻 the `cedikit` command (Typer)
│ ├── exceptions.py # CedikitError and friends
│ ├── py.typed # ships type hints to users
│ ├── 📂 data/
│ │ └── prefixes.yaml # network prefixes (NCA numbering plan + updates)
│ ├── 📂 sms/
│ │ ├── parser.py # template engine, confidence scoring
│ │ ├── models.py # Transaction, Counterparty, ParseResult
│ │ ├── anonymise.py # privacy-preserving anonymiser
│ │ └── 📂 templates/ # mtn.yaml, telecel.yaml
│ ├── 📂 fraud/
│ │ ├── rules.py # checks + noisy-OR scoring
│ │ ├── classifier.py # optional scikit-learn model
│ │ └── scam_phrases.yaml # scam language, misspellings, ID lengths
│ ├── 📂 fees/
│ │ ├── calculator.py # dated, sourced fee + E-Levy estimates
│ │ └── 📂 tables/ # mtn.yaml, telecel.yaml, levies.yaml
│ ├── 📂 ids/
│ │ ├── ghana_card.py # GHA / FGN card numbers
│ │ ├── gpgps.py # GhanaPostGPS digital addresses
│ │ └── regions.yaml # 10 region letters, 218 district codes
│ ├── 📂 integrations/ # pandas, pydantic, django, flask
│ └── 📂 app/ # 🖥️ apps for non-programmers
│ ├── common.py # shared labels, tables, examples
│ ├── desktop.py # 🪟 Tkinter desktop app
│ ├── web.py # 🌐 Streamlit web app
│ └── cedikit.ico # app icon
├── 📂 tests/ # 403 tests, anonymised fixtures in fixtures/sample_messages/
├── 📂 docs/ # MkDocs site (Read the Docs)
├── 📂 examples/ # synthetic demo data + generator
├── 📂 notebooks/ # end-to-end demo notebook
├── 📂 packaging/ # builds the stand-alone cedikit-app.exe
├── .github/workflows/ci.yml # lint, types, tests on 3.10–3.13, build, publish
├── .readthedocs.yaml # docs hosting
└── pyproject.toml # packaging (Hatch), extras, tool config
🧬 Data model
classDiagram
class ParseResult {
status: parsed | unrecognised
transaction: Transaction?
raw: str
ok() bool
}
class Transaction {
network: MTN | TELECEL
type: TransactionType
amount: Decimal
fee, tax, balance: Decimal?
transaction_id, reference: str?
timestamp: datetime?
confidence: float
affects_wallet: bool
needs_review() bool
}
class Counterparty {
name: str?
phone: str? (E.164)
network: str?
}
class TransactionType {
RECEIVED · SENT · CASH_IN · CASH_OUT
MERCHANT · AIRTIME · BILL · REVERSAL
direction() in | out
}
class FraudReport {
risk: LOW | MEDIUM | HIGH
score: float
reasons: list~str~
checks: dict~str, bool~
advice: str
}
ParseResult --> Transaction
Transaction --> Counterparty
Transaction --> TransactionType
FraudReport --> ParseResult : parsed
🧭 Design principles
| Principle | In practice | |
|---|---|---|
| 🔒 | Offline by default | No network calls anywhere. No data leaves the device. |
| 🗂️ | Data separate from code | Prefixes, templates, fees, scam phrases and districts are YAML. |
| 🧩 | Small, focused modules | Use phone alone, or combine everything. |
| 🛟 | Fail gracefully | Unknown SMS returns unrecognised; invalid input explains why. |
| 🎯 | Honest outputs | Networks are likely, fees are estimates, fraud is a risk indicator, unknowns stay None. |
| 💯 | Money is never a float | Decimal end to end; floats raise CediTypeError. |
| 📜 | Evidence over guesses | Every fee rule and dataset cites its source; real messages beat unverified guides. |
✅ Quality and testing
| Check | Result |
|---|---|
| 🧪 Tests (pytest + Hypothesis property tests) | 403 passing |
| 📈 Line coverage | 99% (minimum enforced: 90%) |
| 🔍 Type checking | mypy strict, zero errors |
| 🧹 Lint and format | Ruff, zero issues |
| 📚 Docstring examples | 30 doctests run as tests |
| 📄 Docs build | mkdocs build --strict |
| 🐍 Python versions | 3.10 · 3.11 · 3.12 · 3.13 |
Property-based tests check invariants such as normalise(format(x)) == x for every valid
number, exact Cedi sums, and that the parser never raises on arbitrary text. Every
anonymised sample message must parse to its expected fields, and every genuine one must score
LOW for fraud.
git clone https://github.com/brainiacweb-tech/cedikit && cd cedikit
python -m venv .venv && .venv/Scripts/activate # macOS/Linux: source .venv/bin/activate
pip install -e ".[dev,docs]"
pytest # tests + coverage
pytest --no-cov --doctest-modules src # docstring examples
ruff check . && ruff format --check .
mypy
mkdocs serve # docs at http://127.0.0.1:8000
🔐 Privacy and ethics
- 🔒 Offline only. Nothing is uploaded, logged or stored unless you export it. Even screenshots are read on your own computer.
- 🕶️ Masking helpers:
phone.mask()andghana_card.mask()for logs and reports. - 🧹 Anonymised data only. Every sample message in this repository was anonymised
before being committed.
cedikit sms anonymisedoes it for you and keeps balances consistent. - ⚠️ Always confirm payments in the official Mobile Money app or USSD menu before releasing goods. No SMS check is a guarantee.
- ⚖️ Designed in line with the principles of Ghana's Data Protection Act, 2012 (Act 843).
⚠️ Limitations
- Formats: MTN MoMo and Telecel Cash only (12 templates). AT Money is out of scope.
- Fees are estimates. MTN's cross-network fee and Telecel's cash-out fee are unknown until confirmed from real messages or official tariffs.
- Screenshots: text recognition can misread characters; cedikit fixes common slips
(
GHS50.OO→GHS50.00) but the text should always be compared with the picture. - Ghana Card check digits can't be verified; the algorithm isn't published.
- ID checks confirm format only, never that a card or address exists.
- Fraud accuracy has so far been measured on the messages used to write the rules; a held-out evaluation is in progress. No trained ML model is shipped.
🗺️ Roadmap
- 📱 Phone, 💰 money, 📩 SMS parser, 🚨 fraud checks, 📒 ledger, 🧾 fees, 🪪 IDs
- 💻 CLI, 🔌 integrations, 📚 docs, 📦 PyPI release
- 📏 Held-out evaluation on unseen genuine and scam messages
- 🖥️ Desktop app, stand-alone
.exe, web app,python -m cedikit - 🏦 Bank transaction SMS · 📄 MoMo PDF statements
- 🗣️ Twi and other local-language scam phrases
- 🤖 Telegram bot and web app built on cedikit
- 🌍 Country packs for Nigeria and Côte d'Ivoire
🤝 Contributing
Contributions are welcome, especially new SMS templates when a telco changes its wording and anonymised scam samples.
- Read CONTRIBUTING.md and Adding an SMS template.
- Never commit real personal data. Run
cedikit sms anonymisefirst. - Make sure
pytest,ruffandmypypass.
Found a bug or a message that doesn't parse? Open an issue with an anonymised copy.
📜 Licence and acknowledgements
Released under the MIT Licence © Francis Kusi.
Built by Francis Kusi as a BSc Business Information Technology project, Department of Supply Chain and Information Systems, KNUST School of Business.
Data sources: National Communications Authority numbering plan, GhanaPostGPS, Ghana Revenue Authority, and anonymised messages generously shared by volunteers.
🇬🇭 Built in Ghana, for Ghana.
Release files for cedikit 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cedikit-1.2.0.tar.gz | 925.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cedikit-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / cedikit-1.2.0.tar.gz
| Download URL | cedikit-1.2.0.tar.gz |
|---|---|
| Size | 925.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
824f5ecb0071310566829eb391864f29ac1c206785c839f0aeb3bc80ce0e40bd
|
|
BLAKE2b-256 checksum How to use checksums |
5313b8edb23fbdddfed285d9f4b8348bd640c086fc5e5f339ad586400a8de97d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Release files / cedikit-1.2.0-py3-none-any.whl
| Download URL | cedikit-1.2.0-py3-none-any.whl |
|---|---|
| Size | 133.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e68705c0abbd1b2ce282f2d7f33b46cb79ec07e60163fa618956a5148b538606
|
|
BLAKE2b-256 checksum How to use checksums |
3ef90abc9d216ea0b7211d9610b29c412dd75bab13a835796e6279e13dd659f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|