[{"data":1,"prerenderedAt":444},["ShallowReactive",2],{"nav-stories":3,"project-lleven-v1":61},[4,16,25,34,43,52],{"id":5,"color":6,"extension":7,"image":8,"label":9,"link":10,"meta":11,"order":12,"stem":13,"text":14,"__hash__":15},"stories\u002Fstories\u002F01-data-center.yml",null,"yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=1080","DATA_CENTER","https:\u002F\u002Fx.com\u002Fabbeytetteh_",{},1,"stories\u002F01-data-center","Racking new servers. 40gbit backbone online.","0QUZQbaANhdO8WemZxkDdO7vbVopfnynHtH9FxBZb_w",{"id":17,"color":6,"extension":7,"image":18,"label":19,"link":6,"meta":20,"order":21,"stem":22,"text":23,"__hash__":24},"stories\u002Fstories\u002F02-thoughts.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1498050108023-c5249f4df085?w=1080","THOUGHTS",{},2,"stories\u002F02-thoughts","Late night bug hunting. Found the memory leak.","Gd1am954aasY6HRHD7hCtOuessXb6zYZ8iizS501ICg",{"id":26,"color":27,"extension":7,"image":6,"label":28,"link":6,"meta":29,"order":30,"stem":31,"text":32,"__hash__":33},"stories\u002Fstories\u002F03-coding.yml","#3b82f6","CODING",{},3,"stories\u002F03-coding","Just thinking about how much easier life is with Swarm. https:\u002F\u002Fgoogle.com","-WTk-47jnLM-TZRWBg0VbJyZJfIM7FpQ5HGbc8LEdhQ",{"id":35,"color":6,"extension":7,"image":36,"label":37,"link":6,"meta":38,"order":39,"stem":40,"text":41,"__hash__":42},"stories\u002Fstories\u002F04-update.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1591799264318-7e6ef8ddb7ea?w=1080","UPDATE",{},4,"stories\u002F04-update","New cluster nodes arrived. Prepping for installation.","kyT60N5C6Re_jMonZbgNy0PbQhzXmUWxDbD0D_v43ts",{"id":44,"color":45,"extension":7,"image":6,"label":46,"link":6,"meta":47,"order":48,"stem":49,"text":50,"__hash__":51},"stories\u002Fstories\u002F05-setup.yml","#86868b","SETUP",{},5,"stories\u002F05-setup","Optimizing the telemetry pipeline for 1M req\u002Fs.","cPOBkzoyXsCmPgRO2d80Hj3vm4MP-6nAejtlQ5iuSzw",{"id":53,"color":6,"extension":7,"image":54,"label":55,"link":6,"meta":56,"order":57,"stem":58,"text":59,"__hash__":60},"stories\u002Fstories\u002F06-travel.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1560969184-10fe8719e047?w=1080","TRAVEL",{},6,"stories\u002F06-travel","Travel log — system architecture workshop in Berlin.","jnOxerdF6usAIHdR35Z-opx0LJAy9kZluXnZhtz62Z0",{"id":62,"title":63,"body":64,"description":432,"extension":433,"liveUrl":6,"meta":434,"navigation":435,"order":21,"path":436,"seo":437,"stack":438,"stem":442,"thumbnail":6,"__hash__":443},"projects\u002Fprojects\u002Flleven-v1.md","Lleven V1: The Genesis",{"type":65,"value":66,"toc":417},"minimark",[67,71,76,84,89,117,121,129,133,136,158,162,177,222,226,250,254,313,321,324,384,388,414],[68,69,70],"p",{},"The first version of Lleven was born out of a simple need: to make sense of Mobile Money (MoMo) statements without the manual hassle. It was a lean, focused tool designed to transform a raw PDF into a \"Wrapped\" experience—much like Spotify, but for your spending.",[72,73,75],"h2",{"id":74},"architecture-overview","Architecture Overview",[68,77,78,79,83],{},"The V1 architecture was a classic ",[80,81,82],"strong",{},"Synchronous Processing"," model. It prioritized simplicity and immediate feedback for small-to-medium statements.",[85,86,88],"h3",{"id":87},"tech-stack","Tech Stack",[90,91,92,99,105,111],"ul",{},[93,94,95,98],"li",{},[80,96,97],{},"Framework:"," FastAPI",[93,100,101,104],{},[80,102,103],{},"Processing:"," Pandas & PDFPlumber",[93,106,107,110],{},[80,108,109],{},"Caching:"," Redis",[93,112,113,116],{},[80,114,115],{},"Security:"," Fernet (AES-128) Encryption for cached data",[72,118,120],{"id":119},"the-processing-engine","The Processing Engine",[68,122,123,124,128],{},"Lleven V1 used a specialized parsing engine built on top of ",[125,126,127],"code",{},"pdfplumber",".",[85,130,132],{"id":131},"_1-validation-logic","1. Validation Logic",[68,134,135],{},"Before processing, the system checked for specific \"magic strings\" in the first page of the PDF to ensure it was a valid MTN MoMo statement. These included:",[90,137,138,143,148,153],{},[93,139,140],{},[125,141,142],{},"MSISDN:",[93,144,145],{},[125,146,147],{},"Time Run:",[93,149,150],{},[125,151,152],{},"TRANSACTION DATE",[93,154,155],{},[125,156,157],{},"ACCOUNT HOLDER NAME:",[85,159,161],{"id":160},"_2-data-extraction","2. Data Extraction",[68,163,164,165,168,169,172,173,176],{},"The engine targeted tables with a ",[125,166,167],{},"vertical_strategy"," and ",[125,170,171],{},"horizontal_strategy"," set to ",[125,174,175],{},"\"lines\"",". It mapped raw PDF columns to a structured internal format:",[90,178,179,212],{},[93,180,181,184,185,187,188,187,191,187,194,187,197,187,200,187,203,187,206,187,209,128],{},[80,182,183],{},"Raw Mapping:"," ",[125,186,152],{},", ",[125,189,190],{},"FROM ACCT",[125,192,193],{},"FROM NO.",[125,195,196],{},"TRANS. TYPE",[125,198,199],{},"AMOUNT",[125,201,202],{},"TO NO.",[125,204,205],{},"TO NAME",[125,207,208],{},"REF",[125,210,211],{},"OVA",[93,213,214,217,218,221],{},[80,215,216],{},"Cleaning:"," It used regex to identify the ",[125,219,220],{},"ACCOUNT_HOLDER_NO"," from the header text to distinguish between incoming and outgoing funds.",[85,223,225],{"id":224},"_3-data-cleaning-pipeline","3. Data Cleaning Pipeline",[90,227,228,238,244],{},[93,229,230,233,234,237],{},[80,231,232],{},"Date Normalization:"," Converted string dates (e.g., ",[125,235,236],{},"21-May-2023 10:30:00 AM",") into proper Python datetime objects.",[93,239,240,243],{},[80,241,242],{},"Type Casting:"," Converted currency strings into floats for arithmetic operations.",[93,245,246,249],{},[80,247,248],{},"Normalization:"," Removed newline characters and extra whitespace from names and references using custom regex cleaning.",[72,251,253],{"id":252},"the-upload-workflow","The Upload Workflow",[255,256,257,266,276,282,304],"ol",{},[93,258,259,262,263,128],{},[80,260,261],{},"Request:"," User uploads a PDF to ",[125,264,265],{},"\u002Fprocess-file",[93,267,268,271,272,275],{},[80,269,270],{},"Deduplication:"," A SHA-256 hash of the file is generated. If the hash exists in Redis, the system returns the existing ",[125,273,274],{},"file_hash"," immediately.",[93,277,278,281],{},[80,279,280],{},"Parsing:"," If new, the system extracts the table, cleans the data, and caps it to the requested year (e.g., 2023).",[93,283,284,287],{},[80,285,286],{},"Encrypted Caching:",[90,288,289,295,301],{},[93,290,291,292,128],{},"The resulting DataFrame is serialized using ",[125,293,294],{},"pickle",[93,296,297,298,128],{},"It is then encrypted using ",[80,299,300],{},"Fernet (symmetric encryption)",[93,302,303],{},"The encrypted blob is stored in Redis with a 1-hour TTL.",[93,305,306,309,310,312],{},[80,307,308],{},"Response:"," Returns the ",[125,311,274],{}," and an expiry timestamp.",[72,314,316,317,320],{"id":315},"the-retrieval-workflow-get-wrapped","The Retrieval Workflow (",[125,318,319],{},"\u002Fget-wrapped",")",[68,322,323],{},"When the user requests their \"Wrapped\" results:",[255,325,326,329,332],{},[93,327,328],{},"The system pulls the encrypted blob from Redis.",[93,330,331],{},"It decrypts and deserializes the DataFrame.",[93,333,334,337,338],{},[80,335,336],{},"On-the-Fly Analytics:"," It runs a series of summary algorithms:\n",[90,339,340,359,365,371],{},[93,341,342,345,346,187,349,187,352,355,356,128],{},[80,343,344],{},"Spending Summary:"," Aggregates totals for ",[125,347,348],{},"PAYMENT",[125,350,351],{},"CASH_OUT",[125,353,354],{},"TRANSFER",", and ",[125,357,358],{},"DEBIT",[93,360,361,364],{},[80,362,363],{},"Frequency Analysis:"," Calculates the top 5 recipients by amount and frequency.",[93,366,367,370],{},[80,368,369],{},"Monthly Trends:"," Groups transactions by month to visualize spending patterns.",[93,372,373,376,377,379,380,383],{},[80,374,375],{},"Credit Summary:"," Identifies salary or incoming transfers by filtering for the user's ",[125,378,220],{}," in the ",[125,381,382],{},"TO_NO"," column.",[72,385,387],{"id":386},"limitations-of-v1","Limitations of V1",[90,389,390,396,402,408],{},[93,391,392,395],{},[80,393,394],{},"The \"Timeout\" Wall:"," Large PDFs (50+ pages) often caused HTTP timeouts because the API waited for the entire extraction to finish before responding.",[93,397,398,401],{},[80,399,400],{},"Memory Pressure:"," Since processing happened on the API workers, high concurrent uploads could lead to OOM (Out of Memory) errors.",[93,403,404,407],{},[80,405,406],{},"Stateless Persistence:"," Data only lived in Redis. If the cache expired, the user had to re-upload the file.",[93,409,410,413],{},[80,411,412],{},"Lack of Identity:"," No user accounts meant users couldn't see a history of their past uploads without keeping the file hashes themselves.",[68,415,416],{},"Lleven V1 proved the concept, but the stage was set for a more robust, scalable, and secure V2.",{"title":418,"searchDepth":21,"depth":21,"links":419},"",[420,423,428,429,431],{"id":74,"depth":21,"text":75,"children":421},[422],{"id":87,"depth":30,"text":88},{"id":119,"depth":21,"text":120,"children":424},[425,426,427],{"id":131,"depth":30,"text":132},{"id":160,"depth":30,"text":161},{"id":224,"depth":30,"text":225},{"id":252,"depth":21,"text":253},{"id":315,"depth":21,"text":430},"The Retrieval Workflow (\u002Fget-wrapped)",{"id":386,"depth":21,"text":387},"The first version of Lleven, a synchronous processing engine designed to transform raw Mobile Money PDFs into structured spending analytics.","md",{},true,"\u002Fprojects\u002Flleven-v1",{"title":63,"description":432},[439,440,441],"FastAPI","Pandas","Redis","projects\u002Flleven-v1","HUCFXtkqCH7TKZwiJKSCxzJevh-30bUSAoJtS1E_9Mc",1780657374501]