{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-04-vibe-meter-monitor-your-ai-costs/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "![](https://steipete.me/assets/img/2025/vibe-meter/hero.jpg)\n\nMy Cursor bill hit $900 for the month. I needed a simple way to track AI spending without constantly checking dashboards. That frustration sparked [Vibe Meter](https://www.vibemeter.ai/) - my first shipped Mac app built in three intense days.\n\nWhat started as a workshop demo became a sophisticated menu bar utility using Swift 6, SwiftUI, and extensive AI assistance.\n\n## What the AIs Say\n\nI had several AI models analyze the [VibeMeter codebase](https://github.com/steipete/VibeMeter):\n\n**O3**: “Name vs. purpose mismatch – it sounds like a mood tracker, but monitors AI-API spending.”\n\n**Claude Opus**: “86% test coverage is extraordinary. Code quality rivals what I see in major tech companies. This isn’t just a ‘side project’ - it’s a case study in how to build macOS software the right way.”\n\n**Gemini**: “Intentionally over-engineered for future growth.”\n\n## From Workshop Demo to Production App\n\nI ran [a live workshop](https://steipete.me/posts/2025/the-future-of-vibe-coding/) demonstrating AI-assisted development. Since Cursor lacks a public API, we reverse-engineered their web interface, extracted cookies, and called internal endpoints. Built both Electron and macOS versions in parallel.\n\nThe feedback surprised me - people actually found it useful. When [Gergely Orosz](https://x.com/GergelyOrosz) mentioned linking to it in his newsletter, I spent two more days polishing the rough demo into something shippable.\n\nCursor has no public API for cost control, and it was somewhat messy because I have an enterprise account while many people have individual accounts, and the API endpoints are quite different. Getting both right was tricky.\n\nSpecial thanks to [Oliver Drobnik](https://x.com/Cocoanetics) for helping me fix the individual account parsing. The second 90% of finishing an app always takes longer than the first 90%.\n\n> **Looking for testers:** Need someone with multiple Cursor teams to help test. Current implementation assumes one team only.\n\n## Key Technical Challenges\n\n### SwiftUI vs AppKit: The Eternal Dance\n\nI used SwiftUI wherever possible, but macOS menu bar apps expose the framework’s limitations. Need a popover without the arrow? Drop down to AppKit. Want to show settings from a menu item? More AppKit gymnastics.\n\nI needed a [custom NSPanel](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/CustomMenuWindow.swift) for precise menu bar control. Memory management got tricky in release builds - weak references to windows deallocated before appearing.\n\nEven showing settings required [a reflection hack](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/NSApplication%2BopenSettings.swift) accessing internal NSMenuItem properties. Apple deprecated the old methods, but the “correct” SwiftUI approach only works from SwiftUI views.\n\n### The Animation Breakthrough\n\nI’m proud of the [animated menu bar icon](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/GaugeIcon.swift). Almost used Lottie, but SwiftUI was enough. ChatGPT converted a static icon into animatable code.\n\nThe gauge moves with spending levels, uses conic gradients for loading shimmer. ImageRenderer converts SwiftUI to NSImage for the status item.\n\n### Swift 6 Concurrency Deep Dive\n\nThis was my first production app using Swift 6’s strict concurrency model. The [architecture heavily relies on actors](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/) for thread safety:\n\n- [CursorAPIClient](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Providers/Cursor/CursorAPIClient.swift) and [ExchangeRateManager](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift) as actors\n- `@MainActor` isolation for UI components\n- Structured concurrency with TaskGroup in [MultiProviderDataOrchestrator](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift)\n- `async/await` throughout, ditching Combine entirely\n\nThe learning curve was steep - debugging `UNUserNotificationCenter` delegate callbacks firing on background threads while trying to update `@MainActor` state was particularly painful.\n\nOne bug drove me crazy: perfect in debug, window wouldn’t show in release. SwiftUI view held weakly by AppKit disappeared before display. No crash, just nothing.\n\nClaude figured it out when I explained the symptoms. Reminder: always test release builds.\n\n### Apple’s Distribution Gauntlet\n\nThe hardest part wasn’t coding - it was signing, notarization, and distribution. Apple’s process is error-prone: you can notarize an app but miss deep notarization of embedded frameworks (like Sparkle), leaving you with a partially notarized app that won’t run.\n\nRather than wrestle with Fastlane, I built [custom shell scripts](https://github.com/steipete/VibeMeter/tree/main/scripts) that are much more debuggable. Now Claude can create releases by simply running “make a new release 1.1 beta 1” - the automation is beautiful once set up.\n\nThe scripts handle everything: incrementing build numbers, creating DMGs, uploading to GitHub releases, generating Sparkle appcast files. The most complex part was handling Sparkle’s EdDSA signatures correctly - the error messages when you get it wrong are cryptic at best. Pro tip: always test your update feed locally before pushing.\n\n## Interesting Technical Details\n\n### Multi-Provider Architecture\n\nThough [Vibe Meter](https://github.com/steipete/VibeMeter) only supports Cursor currently, the architecture anticipates multiple providers. The [MultiProviderDataOrchestrator](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift) and protocol-based design make adding OpenAI or Anthropic straightforward when the time comes.\n\nI really had to hold myself back from implementing OpenAI and Anthropic support right away. When I realized this would be useful for tracking all AI costs - not just Cursor - the temptation was strong. But I knew if I started adding providers, I’d never ship. So I built the architecture to support it but forced myself to ship with just Cursor. This is definitely coming in a future update.\n\nThe multi-provider design might seem like overkill, but it’s already paying dividends. When debugging, I can easily add a mock provider that returns test data. The orchestrator handles provider failures gracefully - if Cursor’s API is down, the app doesn’t crash, it just shows cached data with a subtle indicator.\n\n### Polish in the Details\n\nSmall touches matter. The app includes:\n\n- [Currency conversion](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift) with real-time exchange rates\n- [Gravatar integration](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/UserAvatarView.swift) for user avatars (SHA-256 hashing, retina support)\n- Comprehensive [network connectivity monitoring](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/NetworkConnectivityMonitor.swift)\n- [ApplicationMover](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift) for automatic /Applications folder relocation\n- Spending limit notifications with [configurable thresholds](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SpendingLimitsManager.swift)\n\n### Agent-Friendly Development\n\nI constrained every file to under 300 lines - agents make fewer mistakes with smaller files. This forced better organization and created a more maintainable codebase for both humans and AI. Using [Tuist](https://tuist.io) for project generation also helped since agents struggle with Xcode’s complex `.pbxproj` files.\n\n## By the Numbers\n\nWhat emerged surprised even me:\n\n- **174 Swift files** totaling **28,599 lines** of code\n- **86% test coverage** with 628 individual test methods\n- **18 shell scripts** (2,857 lines) for build automation\n- **437 occurrences** of Swift 6 concurrency keywords\n- **15 protocols** for loose coupling and testability\n\nThe largest files respect the 300-line constraint: [ApplicationMover.swift](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift) (390 lines), [SparkleUpdaterManager.swift](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SparkleUpdaterManager.swift) (361 lines), and [Color+Theme.swift](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Theme/Color%2BTheme.swift) (358 lines). The build infrastructure alone required comprehensive scripts for [code signing](https://github.com/steipete/VibeMeter/blob/main/scripts/sign-and-notarize.sh) (349 lines), [release automation](https://github.com/steipete/VibeMeter/blob/main/scripts/setup-sparkle-release.sh) (314 lines), and [notarization](https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh) (312 lines).\n\n## The Testing Philosophy\n\nI wanted to see how well AI generates tests. For a hobby project like this, I probably wouldn’t have written any. But they caught real bugs:\n\n- Currency conversion edge cases (what happens when the exchange rate API is down?)\n- Cookie expiration handling (Cursor cookies last 30 days, not forever)\n- Timezone bugs (monthly spending resets at UTC midnight, not local time)\n- The classic “what if the user has no internet?” scenarios\n\nThe [UserDefaultsBackedAdvancedTests](https://github.com/steipete/VibeMeter/blob/main/VibeMeterTests/Core/Utilities/UserDefaultsBackedAdvancedTests.swift) alone has 374 lines testing every possible edge case of property wrappers. Did we need all of them? Probably not. Did they catch bugs? Absolutely.\n\n## Lessons Learned\n\n**Context windows matter**. I ran six Claude instances concurrently - one for linting, one for tests, one for UI, one for features, one for bugs. First time trying this strategy. Worked beautifully. The more an agent knows about previous related tasks, the better it performs.\n\n**Small files matter**. The 300-line constraint prevented agent editing mistakes and forced better architecture. What seemed like a limitation became a forcing function for good design.\n\n**SwiftUI has come far**. Despite the AppKit gymnastics required for menu bar specifics, SwiftUI handled 90% of the UI beautifully. The animation capabilities particularly impressed me.\n\n**Modern Swift is powerful**. Swift 6’s actor model and structured concurrency made complex state management surprisingly manageable, even for a concurrency novice.\n\n**AI agents love writing tests**. Embrace it. The comprehensive test suite they generated caught bugs I never would have thought to test for.\n\n## Performance Notes\n\nA menu bar app needs to be lightweight. Vibe Meter uses about 45MB of memory and essentially zero CPU when idle. The [BackgroundDataProcessor](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/BackgroundDataProcessor.swift) refreshes data every 5 minutes by default, but backs off exponentially if errors occur.\n\nOne optimization I’m proud of: the app only loads exchange rates when you actually use a non-USD currency. The [ExchangeRateManager](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift) caches rates for 24 hours and falls back gracefully if the API is down. Small touches, but they matter for an app that runs 24/7.\n\nIf you’re tracking AI costs, try [Vibe Meter](https://www.vibemeter.ai/). Source on [GitHub](https://github.com/steipete/VibeMeter). Watch the [workshop recording](https://steipete.me/posts/2025/the-future-of-vibe-coding/) to see where it started.\n\nThree days. One person. Six AI assistants. The future is weird.\n",
  "authors": [],
  "captured_at": "2026-09-27T19:29:15.293681927+00:00",
  "category": "programming",
  "content_html": "<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://steipete.me/assets/img/2025/vibe-meter/hero.jpg\" alt=\"\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>My Cursor bill hit $900 for the month. I needed a simple way to track AI spending without constantly checking dashboards. That frustration sparked <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.vibemeter.ai/\">Vibe Meter</a> - my first shipped Mac app built in three intense days.</p>\n<p>What started as a workshop demo became a sophisticated menu bar utility using Swift 6, SwiftUI, and extensive AI assistance.</p>\n<h2>What the AIs Say</h2>\n<p>I had several AI models analyze the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter\">VibeMeter codebase</a>:</p>\n<p><strong>O3</strong>: “Name vs. purpose mismatch – it sounds like a mood tracker, but monitors AI-API spending.”</p>\n<p><strong>Claude Opus</strong>: “86% test coverage is extraordinary. Code quality rivals what I see in major tech companies. This isn’t just a ‘side project’ - it’s a case study in how to build macOS software the right way.”</p>\n<p><strong>Gemini</strong>: “Intentionally over-engineered for future growth.”</p>\n<h2>From Workshop Demo to Production App</h2>\n<p>I ran <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/2025/the-future-of-vibe-coding/\">a live workshop</a> demonstrating AI-assisted development. Since Cursor lacks a public API, we reverse-engineered their web interface, extracted cookies, and called internal endpoints. Built both Electron and macOS versions in parallel.</p>\n<p>The feedback surprised me - people actually found it useful. When <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/GergelyOrosz\">Gergely Orosz</a> mentioned linking to it in his newsletter, I spent two more days polishing the rough demo into something shippable.</p>\n<p>Cursor has no public API for cost control, and it was somewhat messy because I have an enterprise account while many people have individual accounts, and the API endpoints are quite different. Getting both right was tricky.</p>\n<p>Special thanks to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/Cocoanetics\">Oliver Drobnik</a> for helping me fix the individual account parsing. The second 90% of finishing an app always takes longer than the first 90%.</p>\n<blockquote>\n<p><strong>Looking for testers:</strong> Need someone with multiple Cursor teams to help test. Current implementation assumes one team only.</p>\n</blockquote>\n<h2>Key Technical Challenges</h2>\n<h3>SwiftUI vs AppKit: The Eternal Dance</h3>\n<p>I used SwiftUI wherever possible, but macOS menu bar apps expose the framework’s limitations. Need a popover without the arrow? Drop down to AppKit. Want to show settings from a menu item? More AppKit gymnastics.</p>\n<p>I needed a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/CustomMenuWindow.swift\">custom NSPanel</a> for precise menu bar control. Memory management got tricky in release builds - weak references to windows deallocated before appearing.</p>\n<p>Even showing settings required <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/NSApplication%2BopenSettings.swift\">a reflection hack</a> accessing internal NSMenuItem properties. Apple deprecated the old methods, but the “correct” SwiftUI approach only works from SwiftUI views.</p>\n<h3>The Animation Breakthrough</h3>\n<p>I’m proud of the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/GaugeIcon.swift\">animated menu bar icon</a>. Almost used Lottie, but SwiftUI was enough. ChatGPT converted a static icon into animatable code.</p>\n<p>The gauge moves with spending levels, uses conic gradients for loading shimmer. ImageRenderer converts SwiftUI to NSImage for the status item.</p>\n<h3>Swift 6 Concurrency Deep Dive</h3>\n<p>This was my first production app using Swift 6’s strict concurrency model. The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/\">architecture heavily relies on actors</a> for thread safety:</p>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Providers/Cursor/CursorAPIClient.swift\">CursorAPIClient</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift\">ExchangeRateManager</a> as actors</li>\n<li><code>@MainActor</code> isolation for UI components</li>\n<li>Structured concurrency with TaskGroup in <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift\">MultiProviderDataOrchestrator</a></li>\n<li><code>async/await</code> throughout, ditching Combine entirely</li>\n</ul>\n<p>The learning curve was steep - debugging <code>UNUserNotificationCenter</code> delegate callbacks firing on background threads while trying to update <code>@MainActor</code> state was particularly painful.</p>\n<p>One bug drove me crazy: perfect in debug, window wouldn’t show in release. SwiftUI view held weakly by AppKit disappeared before display. No crash, just nothing.</p>\n<p>Claude figured it out when I explained the symptoms. Reminder: always test release builds.</p>\n<h3>Apple’s Distribution Gauntlet</h3>\n<p>The hardest part wasn’t coding - it was signing, notarization, and distribution. Apple’s process is error-prone: you can notarize an app but miss deep notarization of embedded frameworks (like Sparkle), leaving you with a partially notarized app that won’t run.</p>\n<p>Rather than wrestle with Fastlane, I built <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/tree/main/scripts\">custom shell scripts</a> that are much more debuggable. Now Claude can create releases by simply running “make a new release 1.1 beta 1” - the automation is beautiful once set up.</p>\n<p>The scripts handle everything: incrementing build numbers, creating DMGs, uploading to GitHub releases, generating Sparkle appcast files. The most complex part was handling Sparkle’s EdDSA signatures correctly - the error messages when you get it wrong are cryptic at best. Pro tip: always test your update feed locally before pushing.</p>\n<h2>Interesting Technical Details</h2>\n<h3>Multi-Provider Architecture</h3>\n<p>Though <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter\">Vibe Meter</a> only supports Cursor currently, the architecture anticipates multiple providers. The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift\">MultiProviderDataOrchestrator</a> and protocol-based design make adding OpenAI or Anthropic straightforward when the time comes.</p>\n<p>I really had to hold myself back from implementing OpenAI and Anthropic support right away. When I realized this would be useful for tracking all AI costs - not just Cursor - the temptation was strong. But I knew if I started adding providers, I’d never ship. So I built the architecture to support it but forced myself to ship with just Cursor. This is definitely coming in a future update.</p>\n<p>The multi-provider design might seem like overkill, but it’s already paying dividends. When debugging, I can easily add a mock provider that returns test data. The orchestrator handles provider failures gracefully - if Cursor’s API is down, the app doesn’t crash, it just shows cached data with a subtle indicator.</p>\n<h3>Polish in the Details</h3>\n<p>Small touches matter. The app includes:</p>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift\">Currency conversion</a> with real-time exchange rates</li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/UserAvatarView.swift\">Gravatar integration</a> for user avatars (SHA-256 hashing, retina support)</li>\n<li>Comprehensive <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/NetworkConnectivityMonitor.swift\">network connectivity monitoring</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift\">ApplicationMover</a> for automatic /Applications folder relocation</li>\n<li>Spending limit notifications with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SpendingLimitsManager.swift\">configurable thresholds</a></li>\n</ul>\n<h3>Agent-Friendly Development</h3>\n<p>I constrained every file to under 300 lines - agents make fewer mistakes with smaller files. This forced better organization and created a more maintainable codebase for both humans and AI. Using <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://tuist.io\">Tuist</a> for project generation also helped since agents struggle with Xcode’s complex <code>.pbxproj</code> files.</p>\n<h2>By the Numbers</h2>\n<p>What emerged surprised even me:</p>\n<ul>\n<li><strong>174 Swift files</strong> totaling <strong>28,599 lines</strong> of code</li>\n<li><strong>86% test coverage</strong> with 628 individual test methods</li>\n<li><strong>18 shell scripts</strong> (2,857 lines) for build automation</li>\n<li><strong>437 occurrences</strong> of Swift 6 concurrency keywords</li>\n<li><strong>15 protocols</strong> for loose coupling and testability</li>\n</ul>\n<p>The largest files respect the 300-line constraint: <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift\">ApplicationMover.swift</a> (390 lines), <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SparkleUpdaterManager.swift\">SparkleUpdaterManager.swift</a> (361 lines), and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Theme/Color%2BTheme.swift\">Color+Theme.swift</a> (358 lines). The build infrastructure alone required comprehensive scripts for <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/sign-and-notarize.sh\">code signing</a> (349 lines), <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/setup-sparkle-release.sh\">release automation</a> (314 lines), and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh\">notarization</a> (312 lines).</p>\n<h2>The Testing Philosophy</h2>\n<p>I wanted to see how well AI generates tests. For a hobby project like this, I probably wouldn’t have written any. But they caught real bugs:</p>\n<ul>\n<li>Currency conversion edge cases (what happens when the exchange rate API is down?)</li>\n<li>Cookie expiration handling (Cursor cookies last 30 days, not forever)</li>\n<li>Timezone bugs (monthly spending resets at UTC midnight, not local time)</li>\n<li>The classic “what if the user has no internet?” scenarios</li>\n</ul>\n<p>The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeterTests/Core/Utilities/UserDefaultsBackedAdvancedTests.swift\">UserDefaultsBackedAdvancedTests</a> alone has 374 lines testing every possible edge case of property wrappers. Did we need all of them? Probably not. Did they catch bugs? Absolutely.</p>\n<h2>Lessons Learned</h2>\n<p><strong>Context windows matter</strong>. I ran six Claude instances concurrently - one for linting, one for tests, one for UI, one for features, one for bugs. First time trying this strategy. Worked beautifully. The more an agent knows about previous related tasks, the better it performs.</p>\n<p><strong>Small files matter</strong>. The 300-line constraint prevented agent editing mistakes and forced better architecture. What seemed like a limitation became a forcing function for good design.</p>\n<p><strong>SwiftUI has come far</strong>. Despite the AppKit gymnastics required for menu bar specifics, SwiftUI handled 90% of the UI beautifully. The animation capabilities particularly impressed me.</p>\n<p><strong>Modern Swift is powerful</strong>. Swift 6’s actor model and structured concurrency made complex state management surprisingly manageable, even for a concurrency novice.</p>\n<p><strong>AI agents love writing tests</strong>. Embrace it. The comprehensive test suite they generated caught bugs I never would have thought to test for.</p>\n<h2>Performance Notes</h2>\n<p>A menu bar app needs to be lightweight. Vibe Meter uses about 45MB of memory and essentially zero CPU when idle. The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/BackgroundDataProcessor.swift\">BackgroundDataProcessor</a> refreshes data every 5 minutes by default, but backs off exponentially if errors occur.</p>\n<p>One optimization I’m proud of: the app only loads exchange rates when you actually use a non-USD currency. The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift\">ExchangeRateManager</a> caches rates for 24 hours and falls back gracefully if the API is down. Small touches, but they matter for an app that runs 24/7.</p>\n<p>If you’re tracking AI costs, try <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.vibemeter.ai/\">Vibe Meter</a>. Source on <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter\">GitHub</a>. Watch the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/2025/the-future-of-vibe-coding/\">workshop recording</a> to see where it started.</p>\n<p>Three days. One person. Six AI assistants. The future is weird.</p>\n",
  "content_markdown": "![](https://steipete.me/assets/img/2025/vibe-meter/hero.jpg)\n\nMy Cursor bill hit $900 for the month. I needed a simple way to track AI spending without constantly checking dashboards. That frustration sparked [Vibe Meter](https://www.vibemeter.ai/) - my first shipped Mac app built in three intense days.\n\nWhat started as a workshop demo became a sophisticated menu bar utility using Swift 6, SwiftUI, and extensive AI assistance.\n\n## What the AIs Say\n\nI had several AI models analyze the [VibeMeter codebase](https://github.com/steipete/VibeMeter):\n\n**O3**: “Name vs. purpose mismatch – it sounds like a mood tracker, but monitors AI-API spending.”\n\n**Claude Opus**: “86% test coverage is extraordinary. Code quality rivals what I see in major tech companies. This isn’t just a ‘side project’ - it’s a case study in how to build macOS software the right way.”\n\n**Gemini**: “Intentionally over-engineered for future growth.”\n\n## From Workshop Demo to Production App\n\nI ran [a live workshop](https://steipete.me/posts/2025/the-future-of-vibe-coding/) demonstrating AI-assisted development. Since Cursor lacks a public API, we reverse-engineered their web interface, extracted cookies, and called internal endpoints. Built both Electron and macOS versions in parallel.\n\nThe feedback surprised me - people actually found it useful. When [Gergely Orosz](https://x.com/GergelyOrosz) mentioned linking to it in his newsletter, I spent two more days polishing the rough demo into something shippable.\n\nCursor has no public API for cost control, and it was somewhat messy because I have an enterprise account while many people have individual accounts, and the API endpoints are quite different. Getting both right was tricky.\n\nSpecial thanks to [Oliver Drobnik](https://x.com/Cocoanetics) for helping me fix the individual account parsing. The second 90% of finishing an app always takes longer than the first 90%.\n\n> **Looking for testers:** Need someone with multiple Cursor teams to help test. Current implementation assumes one team only.\n\n## Key Technical Challenges\n\n### SwiftUI vs AppKit: The Eternal Dance\n\nI used SwiftUI wherever possible, but macOS menu bar apps expose the framework’s limitations. Need a popover without the arrow? Drop down to AppKit. Want to show settings from a menu item? More AppKit gymnastics.\n\nI needed a [custom NSPanel](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/CustomMenuWindow.swift) for precise menu bar control. Memory management got tricky in release builds - weak references to windows deallocated before appearing.\n\nEven showing settings required [a reflection hack](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/NSApplication%2BopenSettings.swift) accessing internal NSMenuItem properties. Apple deprecated the old methods, but the “correct” SwiftUI approach only works from SwiftUI views.\n\n### The Animation Breakthrough\n\nI’m proud of the [animated menu bar icon](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/GaugeIcon.swift). Almost used Lottie, but SwiftUI was enough. ChatGPT converted a static icon into animatable code.\n\nThe gauge moves with spending levels, uses conic gradients for loading shimmer. ImageRenderer converts SwiftUI to NSImage for the status item.\n\n### Swift 6 Concurrency Deep Dive\n\nThis was my first production app using Swift 6’s strict concurrency model. The [architecture heavily relies on actors](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/) for thread safety:\n\n- [CursorAPIClient](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Providers/Cursor/CursorAPIClient.swift) and [ExchangeRateManager](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift) as actors\n- `@MainActor` isolation for UI components\n- Structured concurrency with TaskGroup in [MultiProviderDataOrchestrator](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift)\n- `async/await` throughout, ditching Combine entirely\n\nThe learning curve was steep - debugging `UNUserNotificationCenter` delegate callbacks firing on background threads while trying to update `@MainActor` state was particularly painful.\n\nOne bug drove me crazy: perfect in debug, window wouldn’t show in release. SwiftUI view held weakly by AppKit disappeared before display. No crash, just nothing.\n\nClaude figured it out when I explained the symptoms. Reminder: always test release builds.\n\n### Apple’s Distribution Gauntlet\n\nThe hardest part wasn’t coding - it was signing, notarization, and distribution. Apple’s process is error-prone: you can notarize an app but miss deep notarization of embedded frameworks (like Sparkle), leaving you with a partially notarized app that won’t run.\n\nRather than wrestle with Fastlane, I built [custom shell scripts](https://github.com/steipete/VibeMeter/tree/main/scripts) that are much more debuggable. Now Claude can create releases by simply running “make a new release 1.1 beta 1” - the automation is beautiful once set up.\n\nThe scripts handle everything: incrementing build numbers, creating DMGs, uploading to GitHub releases, generating Sparkle appcast files. The most complex part was handling Sparkle’s EdDSA signatures correctly - the error messages when you get it wrong are cryptic at best. Pro tip: always test your update feed locally before pushing.\n\n## Interesting Technical Details\n\n### Multi-Provider Architecture\n\nThough [Vibe Meter](https://github.com/steipete/VibeMeter) only supports Cursor currently, the architecture anticipates multiple providers. The [MultiProviderDataOrchestrator](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift) and protocol-based design make adding OpenAI or Anthropic straightforward when the time comes.\n\nI really had to hold myself back from implementing OpenAI and Anthropic support right away. When I realized this would be useful for tracking all AI costs - not just Cursor - the temptation was strong. But I knew if I started adding providers, I’d never ship. So I built the architecture to support it but forced myself to ship with just Cursor. This is definitely coming in a future update.\n\nThe multi-provider design might seem like overkill, but it’s already paying dividends. When debugging, I can easily add a mock provider that returns test data. The orchestrator handles provider failures gracefully - if Cursor’s API is down, the app doesn’t crash, it just shows cached data with a subtle indicator.\n\n### Polish in the Details\n\nSmall touches matter. The app includes:\n\n- [Currency conversion](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift) with real-time exchange rates\n- [Gravatar integration](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/UserAvatarView.swift) for user avatars (SHA-256 hashing, retina support)\n- Comprehensive [network connectivity monitoring](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/NetworkConnectivityMonitor.swift)\n- [ApplicationMover](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift) for automatic /Applications folder relocation\n- Spending limit notifications with [configurable thresholds](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SpendingLimitsManager.swift)\n\n### Agent-Friendly Development\n\nI constrained every file to under 300 lines - agents make fewer mistakes with smaller files. This forced better organization and created a more maintainable codebase for both humans and AI. Using [Tuist](https://tuist.io) for project generation also helped since agents struggle with Xcode’s complex `.pbxproj` files.\n\n## By the Numbers\n\nWhat emerged surprised even me:\n\n- **174 Swift files** totaling **28,599 lines** of code\n- **86% test coverage** with 628 individual test methods\n- **18 shell scripts** (2,857 lines) for build automation\n- **437 occurrences** of Swift 6 concurrency keywords\n- **15 protocols** for loose coupling and testability\n\nThe largest files respect the 300-line constraint: [ApplicationMover.swift](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift) (390 lines), [SparkleUpdaterManager.swift](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SparkleUpdaterManager.swift) (361 lines), and [Color+Theme.swift](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Theme/Color%2BTheme.swift) (358 lines). The build infrastructure alone required comprehensive scripts for [code signing](https://github.com/steipete/VibeMeter/blob/main/scripts/sign-and-notarize.sh) (349 lines), [release automation](https://github.com/steipete/VibeMeter/blob/main/scripts/setup-sparkle-release.sh) (314 lines), and [notarization](https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh) (312 lines).\n\n## The Testing Philosophy\n\nI wanted to see how well AI generates tests. For a hobby project like this, I probably wouldn’t have written any. But they caught real bugs:\n\n- Currency conversion edge cases (what happens when the exchange rate API is down?)\n- Cookie expiration handling (Cursor cookies last 30 days, not forever)\n- Timezone bugs (monthly spending resets at UTC midnight, not local time)\n- The classic “what if the user has no internet?” scenarios\n\nThe [UserDefaultsBackedAdvancedTests](https://github.com/steipete/VibeMeter/blob/main/VibeMeterTests/Core/Utilities/UserDefaultsBackedAdvancedTests.swift) alone has 374 lines testing every possible edge case of property wrappers. Did we need all of them? Probably not. Did they catch bugs? Absolutely.\n\n## Lessons Learned\n\n**Context windows matter**. I ran six Claude instances concurrently - one for linting, one for tests, one for UI, one for features, one for bugs. First time trying this strategy. Worked beautifully. The more an agent knows about previous related tasks, the better it performs.\n\n**Small files matter**. The 300-line constraint prevented agent editing mistakes and forced better architecture. What seemed like a limitation became a forcing function for good design.\n\n**SwiftUI has come far**. Despite the AppKit gymnastics required for menu bar specifics, SwiftUI handled 90% of the UI beautifully. The animation capabilities particularly impressed me.\n\n**Modern Swift is powerful**. Swift 6’s actor model and structured concurrency made complex state management surprisingly manageable, even for a concurrency novice.\n\n**AI agents love writing tests**. Embrace it. The comprehensive test suite they generated caught bugs I never would have thought to test for.\n\n## Performance Notes\n\nA menu bar app needs to be lightweight. Vibe Meter uses about 45MB of memory and essentially zero CPU when idle. The [BackgroundDataProcessor](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/BackgroundDataProcessor.swift) refreshes data every 5 minutes by default, but backs off exponentially if errors occur.\n\nOne optimization I’m proud of: the app only loads exchange rates when you actually use a non-USD currency. The [ExchangeRateManager](https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift) caches rates for 24 hours and falls back gracefully if the API is down. Small touches, but they matter for an app that runs 24/7.\n\nIf you’re tracking AI costs, try [Vibe Meter](https://www.vibemeter.ai/). Source on [GitHub](https://github.com/steipete/VibeMeter). Watch the [workshop recording](https://steipete.me/posts/2025/the-future-of-vibe-coding/) to see where it started.\n\nThree days. One person. Six AI assistants. The future is weird.\n",
  "dateCreated": "2026-09-27T19:29:15.293681927+00:00",
  "date_created": "2025-06-04T11:00:00+00:00",
  "date_modified": "2025-06-04T11:00:00+00:00",
  "date_published": "2025-06-04T11:00:00+00:00",
  "description": "How I built Vibe Meter, a macOS menu bar app to track AI spending in real-time - from workshop demo to shipped product in three days.",
  "external_url": "https://steipete.me/posts/2025/vibe-meter-monitor-your-ai-costs/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/steipete-me/2025/06/2025-06-04-vibe-meter-monitor-your-ai-costs.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/steipete-me/2025/06/2025-06-04-vibe-meter-monitor-your-ai-costs.md"
  },
  "id": "urn:aggr:item:bd80a092b1ccc51279099b47cf27d44720a0096c",
  "isBasedOn": {
    "@id": "https://steipete.me/posts/2025/vibe-meter-monitor-your-ai-costs/"
  },
  "mainEntity": {
    "@id": "https://steipete.me/posts/2025/vibe-meter-monitor-your-ai-costs/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-04-vibe-meter-monitor-your-ai-costs/#webpage"
    },
    "dateModified": "2025-06-04T11:00:00+00:00",
    "datePublished": "2025-06-04T11:00:00+00:00",
    "headline": "Vibe Meter: Monitor Your AI Costs",
    "timeRequired": "PT7M",
    "url": "https://steipete.me/posts/2025/vibe-meter-monitor-your-ai-costs/",
    "wordCount": 1354
  },
  "name": "Archived snapshot: Vibe Meter: Monitor Your AI Costs",
  "preview": null,
  "reading_minutes": 7,
  "replicated_at": null,
  "source": "steipete-me",
  "source_name": "Peter Steinberger",
  "summary": "How I built Vibe Meter, a macOS menu bar app to track AI spending in real-time - from workshop demo to shipped product in three days.",
  "tags": [],
  "temporalCoverage": "2026-09-27T19:29:15.293681927+00:00",
  "title": "Vibe Meter: Monitor Your AI Costs",
  "url": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-04-vibe-meter-monitor-your-ai-costs/",
  "word_count": 1354
}