BillKit - AI Expense Management
An AI-powered expense management application for macOS. Processes invoice files automatically using AI to extract amounts, dates, and vendor information. Manages multiple businesses with local data storage, duplicate detection, and confidence-based review for accuracy.
The Tedium of Expense Tracking
Managing expenses for multiple businesses is tedious. You photograph invoices, manually type in amounts, dates, vendor names, tax calculations. Every invoice is five minutes of data entry. Multiply that by dozens or hundreds of invoices per month, and you're spending hours on administrative work that a computer should handle.
Commercial solutions exist. QuickBooks. Xero. FreshBooks. They all have invoice scanning, but they're cloud-dependent subscription services. Your financial data lives on someone else's servers. You need internet to work. You pay monthly forever. And they're designed for accountants, not for someone who just wants to track expenses across a few businesses without the complexity.
I wanted something different. Local data. Offline capability. AI processing with my choice of provider. No subscriptions beyond what I already pay for AI APIs. A native Mac app that feels like it belongs on macOS. Complete control over my financial data.
The Decision to Build It
I could have forced one of the existing solutions to work. Set up QuickBooks for each business. Deal with their scanning workflow. Accept the monthly fees and cloud dependency. But I didn't want compromises. I wanted an actual expense management app built for the way I work. So I decided to build one.
This would be another Swift project, building on what I learnt from Kanodo, Chronode, and Audibar. My fourth macOS app. But this one would be more complex. AI integration with multiple providers. File processing. Currency handling across dozens of currencies. Sophisticated Core Data relationships. An entirely new set of challenges.
Starting from Experience
Coming to BillKit after building Kanodo, Chronode, and Audibar meant I already knew Swift and SwiftUI. The Core Data patterns were familiar. The architecture decisions came easier. But AI integration was completely new territory. I'd never worked with vision language models for document processing before. I'd never built a system that needed to handle multiple AI providers interchangeably.
What transferred from previous projects:
- Swift and SwiftUI fundamentals
- Core Data architecture and relationships
- Service layer patterns and dependency injection
- macOS app structure and window management
- File handling and storage strategies
- Test-driven development practices
- The critical importance of proper brace placement
What was entirely new:
- AI API integration (Claude and OpenAI)
- Structured data extraction from documents
- Multi-provider architecture with provider switching
- Currency handling across 50+ currencies
- File format conversion (HEIC to JPEG)
- Batch processing with progress tracking
- Complex validation and error recovery
The biggest learning curve was understanding how to prompt AI models for consistent, structured data extraction. Getting an AI to reliably extract invoice data in a specific JSON format, with all amounts in pennies, with proper currency codes, with confidence scores. That took experimentation and iteration.
Building the Core Features
I made several architectural decisions early on that define how BillKit works. These weren't implementation details or optimisations that could be adjusted later. They were foundational choices about data structure, file management, and user workflow that would influence every feature built afterwards.
Getting these decisions right at the start meant the application could grow naturally without fighting against its own architecture.
Multi-Business Architecture
Most expense apps force you to create separate accounts for multiple businesses. BillKit lets you manage unlimited businesses in a single app instance. Each business is completely isolated. Its own categories, vendors, expenses, and files. Switch between businesses instantly from the sidebar.
This required careful Core Data modelling. Every entity (categories, vendors, expenses, media files) links to a business. All queries filter by the active business. Delete a business, and everything cascades properly. The system defaults (the "Uncategorised" category and "Unknown" vendor) are created per-business, not globally.
The business-scoping pattern appears throughout the codebase. It's not an afterthought. It's the fundamental organisational principle.
Dual AI Provider Support
Rather than lock into Claude or OpenAI, BillKit supports both. You choose your provider in settings, enter your API key, and the app uses whichever service you selected. Same prompts. Same structured response format. The abstraction layer handles the differences.
I created an AIServiceProtocol that both ClaudeService and OpenAIService implement. The processing model asks for an AI service and gets back whichever provider is currently selected. The rest of the code doesn't care which API it's using.
This adds complexity. Two SDKs to manage. Two slightly different response formats to handle. Two sets of rate limits and error conditions. But it gives users choice and redundancy. If one service is down or rate-limited, switch to the other and keep working.
UUID-Based File Storage
Files could have been stored with their original names. Easy. Simple. But it creates problems. Two files named "invoice.pdf" from different vendors would conflict. Spaces and special characters in filenames cause issues. User renames a file, and database references break.
BillKit stores every file with its UUID as the filename. The Media entity preserves the original filename for display, but on disk it's something like 9E52657C-CD41-4945-8878-80F2BDD2D0C9.pdf. Files live in business-specific directories: businesses/{business-uuid}/invoices/{file-uuid}.ext.
This eliminates filename collisions entirely. No special character issues. No name conflicts. The file path is reconstructed dynamically from the business ID and file ID, making the entire structure portable if the app container location changes.
Intelligent Duplicate Detection
Users photograph invoices on their phones, then forget they already uploaded them. BillKit detects duplicates at upload time, before any processing happens. It checks filename, file size, and business combination. Not perfect (same file with different name won't match), but effective for the common case of accidentally re-uploading the same file.
Duplicates show in the upload table with red "Duplicate" status and a tooltip explaining the issue. The Process button won't activate for duplicate files. They never reach the processing queue. Simple visual feedback prevents wasted AI API calls.
Three-Phase Processing Model
File processing happens in three distinct phases, each serving a specific purpose in the workflow. This separation provides clarity and control. Users can see exactly where files are in the pipeline, validation catches problems before expensive AI calls are made, and processing can be batched or performed individually.
The phases create natural checkpoints where users remain in control rather than surrendering everything to automation.
Phase 1: Upload and Validation
User adds files through drag-and-drop or file picker. Files are validated (size, format, readability). HEIC images are converted to JPEG. Duplicates are detected. Valid files are copied to temporary storage and displayed in the upload table.
Phase 2: Queue Management
Valid files are added to the processing queue as Media records with status="pending". The queue view shows all pending files with their attributes. User reviews the queue and decides when to process. This separation lets you accumulate files and process them in batches rather than one at a time.
Phase 3: AI Processing and Expense Creation
User clicks "Process All" or processes individual files. Each file is sent to the selected AI provider with a structured prompt. The AI returns JSON with extracted data. The system matches or creates vendors, selects categories, calculates totals, and creates expense records. Low-confidence extractions (< 60%) are flagged for review.
Processing happens sequentially, not in parallel. This prevents API rate limiting and provides clear progress feedback. You watch files move through the pipeline one at a time.
The AI Integration Journey
The hardest part of BillKit was getting AI data extraction working reliably. Not because the APIs are difficult (they're well-documented), but because prompt engineering for structured data is an art.
Prompt Engineering for Structured Responses
The AI needs to return data in a specific format. All monetary amounts in pennies. Currency as ISO 4217 codes. Dates as YYYY-MM-DD. Proper JSON structure with no markdown formatting. Confidence scores per field. Vendor and category matching from provided lists.
Getting this right took iteration. Early prompts returned amounts as strings like "£12.34" instead of integers. Currency codes were sometimes full names instead of codes. Dates came in various formats. The JSON was wrapped in markdown code blocks that had to be stripped.
The final prompts are explicit. They specify the exact JSON structure expected. They emphasise that amounts must be in pennies. They provide the list of existing vendors and categories for matching. They request a confidence score from 0-100. They warn against markdown formatting.
Even with careful prompting, responses vary. Claude is better at following structure instructions. OpenAI occasionally adds markdown formatting despite instructions not to. Both sometimes hallucinate data when fields aren't clear on the invoice. That's why the confidence score and review system exist.
Handling PDFs vs Images
PDFs and images require different handling in the AI APIs. PDFs are sent as documents with specific media types. Images are sent as base64-encoded image data. Both Claude and OpenAI support both formats, but the encoding is different.
BillKit reads the file, determines its type, and constructs the appropriate API request. PDFs up to 100 pages are sent in full. Images are checked for size limits (different for each provider). HEIC images are converted to JPEG before sending because neither API accepts HEIC directly.
The conversion happens in-memory. No temporary files. No filesystem clutter. Just load the HEIC data, ask NSBitmapImageRep to convert to JPEG, get the output data, and send it. Simple once you know the pattern.
Token Usage Tracking
Every AI call costs money. Claude charges per input and output token. OpenAI has similar pricing. Users want to know how much they're spending.
BillKit tracks token usage per expense. When an expense is created from AI extraction, the token counts are stored. You can see exactly how many tokens each invoice consumed. This helps you estimate costs and choose between providers based on actual usage.
The tracking is automatic. The AI services return token counts in their responses. The processing model records them. The expense detail view displays them. No manual tracking required.
File Handling Nightmares
Around the middle of development, I discovered a critical bug in the file upload process. The transaction order was wrong. It wasn't immediately obvious because successful uploads worked perfectly. But whilst testing error scenarios (simulating disk full conditions and permission errors), I noticed something troubling. Failed file operations left orphaned database records behind.
The system was creating durable records before completing ephemeral operations, violating basic transaction principles. This could silently corrupt data integrity in production
Original Bug
The upload process created a Media record in Core Data first, then attempted to move the file from temporary storage to permanent storage. If the file move failed (disk full, permissions error, anything), the database record still existed, but the file didn't. Orphaned records pointing to non-existent files.
This violated basic transaction principles. Create the durable record first, then the ephemeral operation. But in file systems, the durable operation is moving the file. The ephemeral operation is creating the database record. I had it backwards.
The Fix
The solution was simple in concept, tricky in implementation. Move the file first. If that succeeds, create the database record. If the move fails, nothing was created. No orphaned records.
But this meant generating the UUID before creating the Media record (normally Core Data assigns the ID). It meant catching file move errors separately from database errors. It meant careful error handling to ensure no half-completed operations.
After the fix, file uploads became transactional. Either both operations succeed, or neither does. No inconsistent state. No orphaned records. No data integrity issues.
Performance Crisis
I noticed something whilst checking CPU usage in Instruments. The app was smooth with no visible lag and scrolling was responsive. Everything worked well. But the CPU consumption bothered me. 20-40% whilst scrolling through tables was excessive, even though users wouldn't notice any performance issues.
Identifying the Problem
The expense table was recalculating display data on every frame. Date formatting. Currency formatting. Localization lookups. Relationship traversal to get vendor and category names. At 60 frames per second, that's thousands of service calls per second during scrolling.
SwiftUI's reactive system was working against me. Every view update triggered property recalculation. Every scroll event updated the visible rows. Every property access called formatting functions, localization lookups, and Core Data relationship traversals.
With 400 expenses in the table, scrolling meant:
- 24,000+ formatter calls per second
- 14,400+ localization lookups per second
- 7,200+ relationship traversals per second
No wonder the CPU was on fire.
The Solution: Cached Properties and Pagination
The fix was moving all display logic into Core Data entity extensions as computed properties. Swift and SwiftUI cache computed property results within a view update cycle. They only recalculate when the underlying data changes.
I added display properties to every entity:
- Expense: 8 properties (formatted dates, display amounts, vendor/category names)
- Vendor: 8 properties (formatted contact info, default indicators)
- Category: 3 properties (display name with fallbacks)
- Business: 7 properties (current selection indicators, default labels)
- Media: 10 properties (file attributes, status checks, formatted sizes)
Instead of calling DateService.shared.format(expense.invoiceDate) in the view, the view accesses expense.formattedInvoiceDate. The entity extension handles the formatting. SwiftUI caches the result. The view renders smoothly.
Additionally, I added pagination to all table views to limit the number of rows rendered simultaneously, further reducing CPU overhead for large datasets. After the refactor, CPU usage during scrolling dropped to near zero. The table remained butter smooth, and now the CPU metrics matched the user experience.
The Security Audit
Periodically during builds, I always run comprehensive security, performance, memory and optimization audit checks. I do this throughout the build to ensure I've not introduced issues along the way. Often, the findings are illuminating.
Critical Issues
As an example, during one audit, three critical issues emerged that required immediate attention. Each represented a potential crash or security vulnerability that could affect users in production.
Path Traversal Vulnerability
File paths weren't validated sufficiently. A malicious UUID-like string could potentially escape the business directories and access arbitrary locations in the file system. The fix added symlink resolution and path prefix verification to both the business directory and temp directory methods.
Now any attempt to access files outside the documents directory is caught, logged for security monitoring, and returns an error. This provides defence in depth alongside the existing UUID format validation.
Force Unwrapping
Several places used force unwraps (!) that would crash if the optional was nil. Most were safe due to prior validation, but "safe" isn't good enough for production. I eliminated all critical force unwraps in FileService and ExpenseFormView, replacing them with proper guard statements and error handling.
Added a new documentsDirectoryUnavailable error case with localised messages. The app now fails gracefully with clear error messages instead of crashing unexpectedly.
Main Thread Blocking
File operations were happening on the main thread, freezing the UI during processing. Loading 32 MB images into memory. Converting HEIC to JPEG. Moving files. All blocking the interface. I refactored the entire upload process to use background threading with Task.detached, proper actor isolation, and background Core Data contexts.
File I/O and CPU-intensive operations now happen off the main thread. The UI stays responsive even when processing large files, with proper state management to update the interface when operations complete.
High Priority Issues
The audit also revealed several high priority concerns that, whilst not immediately critical, represented significant technical debt and potential stability issues. All four issues were addressed and resolved during the audit session.
Memory Leaks
Strong capture of self in task closures within the batch processing system could prevent proper deallocation of the ProcessingModel. During long running batch operations processing dozens of files, this would cause the model to remain in memory indefinitely even after processing completed.
The fix involved adding [weak self] captures to all task closures and adding guard statements to safely unwrap and exit early if the model had been deallocated. This ensures clean memory management even if the user navigates away during processing.
SQL Injection Risk
User controlled strings were being directly interpolated into NSPredicate format strings when searching for vendors and categories by name. A malicious user could craft special strings containing predicate syntax that would manipulate the query logic, potentially accessing or modifying data they shouldn't.
The fix replaced direct parameter passing with the argumentArray parameter in all four affected locations (two in VendorModel, two in CategoryModel). This treats user input as data rather than code, using Core Data's parameterised query mechanism to prevent injection attacks entirely.
URL Validation
The initial URL validation only checked if a URL could be constructed from the string, which would accept dangerous schemes like javascript:, file:, data:, and others that could be exploited for XSS attacks or unauthorised file access.
The strengthened validation now explicitly whitelists only http and https schemes, verifies the host component exists and is non-empty, and confirms the host contains at least one dot (indicating a proper domain with TLD). This prevents malicious URLs from being stored in vendor records and potentially executed when users click website links.
Currency Cache Performance
The currency cache building algorithm used nested loops. It iterated through ~50 common currencies and then searched through ~800 available locales for each one. This resulted in O(n²) complexity with roughly 40,000 iterations. This caused a noticeable 2-3 second delay on first app launch whilst the cache was being built.
The optimised version builds a reverse lookup map in a single O(n) pass through the locale list, reducing iterations from 40,000 to just 800. Additionally, the cache is now persisted to UserDefaults, so subsequent app launches load instantly from the cached data rather than rebuilding from scratch.
Unique Features That Emerged
Confidence-Based Review System
Not every AI extraction is perfect. Some invoices are blurry. Some have weird layouts. Some contain ambiguous information. Rather than blindly accepting all extractions, BillKit flags low-confidence results for review.
When the AI returns a confidence score below 60%, the expense is marked needsReview = true. It appears in a dedicated review queue. The user sees the invoice side-by-side with the extracted data and can make corrections before accepting it.
This catches errors before they enter the accounting system. It also teaches you which types of invoices the AI struggles with, so you can photograph them more clearly next time.
The Inspector Sidebar Pattern
Initially, expense filtering used a popover. Cramped. Awkward. Not discoverable. During the UI refinement phase, I replaced it with a native macOS inspector sidebar.
The inspector appears on the right side of the expense list when toggled. It shows all filtering options: vendor filter, category filter, status filter, date range filter. Changes apply immediately. The sidebar is resizable. It feels native because it uses macOS's standard inspector pattern.
Implementing this required creating ExpenseSidebarView as a separate component. The form fields use proper validation. The filters update the expense list reactively. Everything works together smoothly.
Inline Vendor and Category Creation
When reviewing an AI-extracted expense, you might realise the vendor doesn't exist in your list yet. Rather than cancel, navigate to vendors, create it, navigate back, and start over, you can create vendors and categories directly in the create, review or edit expense sheets.
The expense form includes "Create New Vendor" and "Create New Category" section. Fill them out, click Create and the new entity appears in the picker, auto-selected. In edit or review modes, the expense auto-saves with the new selection. Seamless workflow.
This required making the create methods for vendor and category async, handling entity creation with auto-selection, and coordinating state between the main form and the vendor/category creation forms. But the user experience is worth it.
The Custom Sidebar
BillKit's sidebar started as a standard SwiftUI List. It worked, but it had visual issues. The system accent colour would flash purple before changing to the custom blue. Customising the hover states was difficult. The selection animation wasn't smooth.
I completely rewrote the sidebar using ScrollView and custom components. Created SidebarButton, a reusable button component with proper hover states, selection styling, and badge support. Created SidebarSectionHeader for consistent section styling. The new sidebar looks native but is entirely under my control.
No more purple flash. Clean hover states with 0.15-second animations. Circular badges for upload and queue counts. Perfect colour management where selected items show white text on blue, and unselected items show blue icons on grey. It matches macOS's sidebar design language whilst being fully customisable.
Technical Challenges and Solutions
Actor Isolation and Concurrency
Swift's strict concurrency model in Swift 5.9+ requires careful handling of actor isolation. Core Data entities are not Sendable. You can't pass NSManagedObject instances across actor boundaries directly.
The solution is extracting data before crossing boundaries. When moving work to a background thread, create a plain struct with all needed data first. Pass the struct, which is Sendable. Use the data in the background context. Return results as structs. Update Core Data on the main thread.
This pattern appears throughout the upload and processing code. Extract. Process. Return. Update. Clean separation of concerns that respects the concurrency model.
Dynamic Path Construction
Core Data can't store absolute file paths reliably. The app sandbox location changes between installations. Hardcoded paths break on other machines or after container moves.
BillKit stores only the business UUID and file UUID. The full path is reconstructed at runtime using a computed property. Media.getFileURL() builds the path from the documents directory, business ID, and filename. This works anywhere the app is installed.
If I add iCloud sync later, the path construction logic is already in one place. Just change how it builds the base directory, and everything else still works.
HEIC Conversion
Both Claude and OpenAI APIs don't accept HEIC images. macOS and iOS create HEIC files by default. Users upload HEIC invoices. The app needs to convert them.
The conversion code loads the HEIC file as NSImage, creates a JPEG representation, and returns the data. It happens in-memory. No temporary files. The JPEG data goes straight to the AI API. Clean and efficient.
Currency Handling with swift-currency
Rather than maintain a manual list of currencies, BillKit uses the swift-currency package. It provides type-safe access to all ISO 4217 currencies. Handles minor units (pennies) automatically. Formats according to locale. Properly deals with edge cases like zero-decimal currencies (JPY) and three-decimal currencies (KWD).
This future-proofs the app. When new currencies are added to the ISO standard, the package updates automatically. I don't maintain a currency list. I don't handle decimal places manually. The package handles it correctly.
Test Coverage
BillKit has 143 unit tests covering all critical components. Tests use in-memory Core Data stores for isolation. Services use dependency injection for mockability.
I made a critical mistake early on. Tests were using the shared CoreDataService instance, which persisted to disk. Hundreds of test entities cluttered the production database. The fix was adding in-memory Core Data support and updating all tests to use isolated stores.
Now tests are properly isolated. They run in parallel safely (with -parallel-testing-enabled NO to avoid KeychainService race conditions) and provide regression protection serving as documentation for how components work.
Current Status and What's Left
All core functionality is complete and tested:
- Multi-business management with isolation
- AI integration with Claude and OpenAI
- File upload with validation and duplicate detection
- Batch processing with progress tracking
- Expense management with review queue
- Category and vendor management with inline creation
- Settings with provider selection and API key storage
- Custom sidebar with proper styling
- Inspector sidebar for expense filtering
- Performance optimisation (table rendering with pagination)
- Security audit fixes (all critical issues resolved)
- 143 unit tests passing
The app works. It's stable. It processes invoices accurately. The UI is polished. Security issues are fixed. Performance is excellent.
What Needs to Happen for MVP
Core Features to Add
- Search functionality throughout the application
- Data export capabilities (CSV, PDF reports)
- Dashboard with reports and analytics
- Expense details sheet for comprehensive viewing
Distribution Preparation
- Website build (single-page site plus policy pages)
- Marketing and promotional materials
- App Store submission and review
- Final comprehensive testing with real-world data
Future Development
- CSV import for categories, vendors, businesses, and expenses
- Additional reporting and analytics features
- Advanced filtering and search capabilities
What Needs to Happen
Distribution Preparation
The app runs locally and has been working reliably throughout development, but it's not ready for distribution just yet. For App Store submission, several important tasks remain:
- Code signing with Developer ID
- Notarization for distribution outside App Store
- Entitlements review for sandboxing
- Privacy manifest for API usage
- App Store screenshots and descriptions
Documentation
Create some user guide and documentation to help users navigate through the app and use it to its full potential:
- User guide explaining the workflow
- Getting started tutorial for first-time users
- AI provider setup instructions (obtaining API keys)
- FAQ for common questions
- Video walkthrough of core features
Final Testing
Additional comprehensive testing routines with real-world usage:
- Process 100+ invoices from various vendors
- Test with both AI providers at scale
- Verify accuracy across different invoice formats
- Test edge cases (corrupt files, network errors, disk full)
- Confirm all error messages are clear and actionable
Website
Although the app is to be distributed via the app store, I like to have online presence for all apps I build. I have the billkit.app domain, so I just need to create
- Single landing page
- Terms of service
- Privacy Policy
None of this is technically difficult. It's just work that needs doing methodically.
Timeline
I started BillKit on October 29, 2025. Built intensively for six days. Completed all core functionality, conducted a security audit, fixed all critical issues, and achieved production-ready status on November 3, 2025.
Total development time: 6 days for a full-featured AI-powered expense management system. That's the benefit of building a third macOS app. You know the patterns. You know the pitfalls. You build faster.
What I Learnt
AI Integration is About Prompt Engineering
The Claude and OpenAI SDKs are straightforward. You send a request, get a response. The hard part is crafting prompts that produce consistent, structured data. Understanding how to ask for JSON in a specific format. Knowing which instructions the models actually follow. Learning which edge cases need explicit handling.
Prompt engineering is iterative. You try something, see what breaks, adjust the prompt, try again. Document formats vary wildly. Invoices from different vendors have different layouts. The AI needs guidance to extract data consistently across all formats.
This skill transfers. Once you understand how to get structured data from vision language models, you can apply that to other document processing tasks. Receipt scanning. Form filling. Contract analysis. The patterns are similar.
Performance Matters More Than You Think
That 20-40% CPU usage during table scrolling seemed acceptable at first. The table still scrolled. The app still worked. But it wasn't smooth. It consumed battery. It made the laptop warm. It was objectively bad even though it superficially worked.
Fixing the performance issues transformed the user experience. The table became glass-smooth. CPU usage dropped to near zero. The app felt professional instead of sluggish. The difference between acceptable and excellent is often just fixing performance problems you initially dismissed as tolerable.
Security Can't Be an Afterthought
Conducting a comprehensive security audit revealed issues I hadn't considered. Path traversal vulnerabilities. Force unwraps that could crash. Main thread blocking. Memory leaks. SQL injection risks. Weak validation.
None of these would have caused immediate problems. But they could have caused problems eventually. In production. With user data. The audit caught them before they became real issues. Security isn't something you bolt on later. It's something you verify before shipping.
File Operations are More Complex Than They Appear
File handling seems simple. Read a file. Write a file. Move a file. But there's a lot that can go wrong. Permissions. Disk space. Corruption. Race conditions. Transaction ordering. Path manipulation.
Getting file operations right requires thinking about failure cases. What happens if the disk is full? What if the file moves during an operation? What if the user lacks permissions? What if two operations conflict? Every file operation needs proper error handling and recovery logic.
Swift Concurrency is Powerful But Strict
Swift's concurrency model prevents entire categories of bugs. No data races. No unexpected shared state. No callback hell. But it's strict. You must respect actor boundaries. You can't pass non-Sendable types freely. You need to structure code to work with the model.
The strictness is worth it. The compiler catches concurrency bugs before they happen. The code is safer. But you need to learn the patterns. Extract-process-return for background work. Computed properties for main-thread caching. Weak captures for long-running operations. The patterns work, but you must learn them.
Would I Build This Again?
Yes, absolutely. BillKit solves a real problem for people managing multiple businesses. The AI integration works reliably. The UI feels native. The architecture is solid. The code is maintainable.
I learnt AI integration. I learnt prompt engineering. I improved my performance optimisation skills. I deepened my understanding of security concerns. Every project teaches something new, and BillKit taught me quite a bit.
If I'd known upfront how complex currency handling would be, or how tricky consistent AI extraction is, or how many edge cases file operations have, I might have been more cautious. But I'm glad I wasn't. Building through complexity is how you learn.
The Development Experience
Solo Development Velocity
Building alone means making decisions quickly. No meetings about whether to support both Claude and OpenAI. No debates about whether UUID-based file naming is worth it. No committees for UI decisions. You just build what makes sense and iterate when it doesn't.
The flip side is you're responsible for everything. Architecture. UI design. Testing. Documentation. Security. Performance. Marketing. Support. There's no one else to handle the things you don't enjoy. You either do them or they don't get done.
Learning Through Building
I learnt AI integration by building a real system that needs it. Not tutorials. Not sample apps. An actual invoice processing system with all the messy requirements real software has. This taught me things tutorials never would.
Like how AI responses vary even with detailed prompts. How different document formats affect extraction accuracy. How to handle low-confidence extractions. How to structure prompts for consistent output. These lessons came from encountering problems and solving them, not from following guides.
The Audit was Essential
That comprehensive security audit wasn't optional. It found critical issues I'd missed. Path traversal. Force unwraps. Thread blocking. Memory leaks. Weak validation. Issues that would have caused problems in production.
Audits force you to look at code critically. Not "does it work?" but "could it fail?" Not "does it run?" but "is it secure?" The answer is often "yes, it could fail" and "no, it's not fully secure yet." Then you fix it before users encounter the problems.
What's Next
Finishing and Shipping
BillKit will be distributed through the App Store, which means Apple's review process and 30% commission, but also provides discoverability, automatic updates, and customer trust. The remaining work before submission includes:
MVP Feature Completion:
- Implement comprehensive search across all data
- Add data export functionality (CSV, PDF)
- Build dashboard with analytics and reports
- Create expense details sheet
Distribution Requirements:
- Build single-page marketing website with policy pages
- Create promotional materials and App Store assets
- Final round of testing with real-world invoice data
- App Store submission and review process
Built for Me, Useful for Others
I built BillKit to solve my own problem. I needed expense management for multiple businesses with AI processing, local data, and no subscriptions. That's what BillKit does.
But the more I worked on it, the more I realised others would benefit too. Small business owners tired of manual data entry. Freelancers who want simple expense tracking. Anyone who values privacy and local data storage. The use cases are broader than just my own needs.
By End of 2025
BillKit will be available on the App Store by the end of 2025. The MVP features are being completed, the marketing site is being built, and promotional materials are being prepared. Once the remaining features are implemented and tested, it will be submitted for App Store review.
I've built four macOS apps in 2025: Audibar, Kanodo, Chronode, and now BillKit. With the core functionality complete and production-ready, finishing the MVP features and preparing for launch by year-end is the goal.
Final Thoughts
On Building with AI
BillKit wouldn't exist without Claude and OpenAI's vision language models. The ability to send an invoice image and get back structured data changed what's possible. Manual data entry is tedious. Having AI do it is transformative.
But AI isn't magic. It's a tool that needs proper integration. You need structured prompts. You need error handling. You need confidence scoring. You need human review for edge cases. The technology is powerful, but using it well requires care.
On Swift and SwiftUI
This was my fourth Swift project. The language feels natural now. The type system that seemed restrictive initially actually prevents bugs. Optionals force you to handle nil cases. The compiler catches mistakes before they become runtime errors.
SwiftUI is genuinely better for building interfaces once you understand it. State flows predictably. Updates happen automatically. The declarative approach matches how you think about UIs. There's a learning curve, but it's worth climbing.
On Performance
That CPU usage issue taught me an important lesson. Just because something appears smooth doesn't mean it's efficient. The app scrolled perfectly. Users would never have noticed a problem. But profiling revealed wasteful computation. 20-40% CPU for displaying a table was objectively excessive.
Performance isn't just about what users perceive. It's about resource efficiency, battery life, and system load. Fixing the CPU usage improved all three without changing the user experience. The difference between "works fine" and "works efficiently" is often just measuring and optimising what you initially dismissed as acceptable.
On Security
The security audit revealed how many potential issues you don't see while building. You're focused on making it work, not on how it could break. You test the happy path, not the malicious edge cases.
Security requires a different mindset. Not "does this work?" but "how could someone abuse this?" Not "is this convenient?" but "is this safe?" The audit forced that mindset, and the code is better for it.
The Timeline
- October 29, 2025. Started BillKit. Built foundation: Core Data model, services, utilities. First phase complete in one day.
- October 30, 2025. Implemented UI and navigation. Business management. Settings. Categories and vendors. Upload interface. Processing queue. Phase complete with 143 tests passing.
- October 31, 2025. Fixed critical bugs. UUID-based file storage. Duplicate detection. Core Data threading issues. Download functionality. Path traversal fixes.
- November 1, 2025. Refactored for maintainability. Extracted toolbars and alerts. Modularised ExpenseFormView. Created utility helpers. Added inline entity creation.
- November 2, 2025. Performance optimisation and UI polish. Fixed accent colour system-wide. Custom sidebar implementation. Cached entity properties. Eliminated 24,000+ service calls per second during scrolling.
- November 3, 2025. Security audit and fixes. Path traversal protection. Force unwrap elimination. Background threading for file operations. All critical issues resolved. Production ready.
- Total time: 6 days from start to production-ready core. 143 tests passing. All core features working. Security issues fixed. Performance excellent. MVP features in development.
That's BillKit. An AI-powered expense management system for macOS. Local data. Dual AI provider support. Multi-business management. Built in six days. Core complete, MVP features being added, App Store launch planned for end of November 2025.