Local-first macOS menu bar app that tells you how long your prepaid cloud GPU credit will last. Reads Vast.ai, computes burn and runway, warns before you run dry.
Kotlin
0
29 commits
updated Sep 21, 2026
CreditWatch is a local-first desktop utility for monitoring prepaid cloud credits. It shows your balance, known burn rate, estimated runway, and obvious signs of underused resources in a compact menu bar popover. Vast.ai is the first provider.
Status: 1.0.0, released 21 September 2026 — macOS, one Vast.ai account.
Connect a Vast.ai key and CreditWatch holds it in Keychain, refreshes every 60 seconds with backoff and stale marking, and answers one question from the menu bar: how long does the credit last at the current burn. Readings are kept locally in SQLite for 72 hours, and the one-hour burn average feeds a safe runway that carries a 10% buffer. Low-runway alerts fire at 12h, 6h and 1h with persisted deduplication — each threshold switches off individually, and one the runway has already fallen under cannot be armed, because it would report the past instead of warning early. Alerts can also reach a phone through an ntfy topic paired by QR code, while the app is running.
What 1.0.0 does not do. One provider account at a time; the settings pane lists the others and marks them unavailable. Phone alerts need the app awake, since a sleeping computer measures nothing. macOS is the only platform claimed — the Windows and Linux credential stores are written and unit tested, and the Linux Secret Service round trip passes headless, but no one has run the desktop there. Bandwidth is treated as an unknown cost and left out of burn. Stopped-instance behaviour against a live account is still unverified.
Next, in 1.1.0: several providers at once, Windows and Linux as claimed platforms, burn-spike alerts, and the telemetry agent. See the roadmap.

The popover is the whole application — there is no main window. It opens under the menu bar icon and closes when it loses focus. The account above is a real one that has reached zero, which is the state most worth showing, and every part of it is readable:
0m is not used for a depleted account, because a
countdown at zero still looks like a countdown.SAFE RUNWAY repeat the state in a
second channel for anyone who cannot rely on the colour.
Settings slides in over the popover rather than opening a window. Providers are rows, so a second account is an obvious next step rather than a redesign. Only Vast.ai has an adapter today and only it can be connected; the rest are listed and visibly unavailable, because a user who came for RunPod should learn that here rather than by hunting for it. Each unavailable row carries a hover hint explaining what is missing. Monitoring still runs a single account — the roadmap names the four things a second one needs.

alerted when that threshold is the one currently
tripped, and held when it cannot be armed — a threshold the runway has already fallen under
is refused, because switching it on would fire at once instead of warning early. A threshold
armed before the fall still fires; the reading governs arming only.Kotlin/JVM and Java 21, Gradle, Compose Multiplatform Desktop, Ktor Client, Kotlin coroutines, and SQLite/JDBC. Java source can live beside Kotlin source in each module under src/main/java. Dependencies for later milestones will be added when their implementation begins.
Install JDK 21, then from the repository root:
./gradlew test
./gradlew :app-desktop:run
Look for the CreditWatch pulse icon near the right end of the menu bar. Click it for the popover, positioned beneath the icon; the tray menu shows it, refreshes, opens settings, cycles the theme, or quits. Settings slides in over the popover rather than opening a window: providers, the runway alert switches, phone pairing, and appearance as a single icon that cycles System, Dark and Light. The provider connection screen links to Vast's API key guide. Where no system tray is available the popover opens directly as a plain window, and closing it quits.
The desktop app uses Kotlin/JVM and Compose Desktop on JDK 21. The application code targets macOS, Windows and Linux, and secure key storage uses the native service on each: Keychain, Credential Manager, or Secret Service through the secret-tool executable from libsecret tools, which Linux also needs an unlocked keyring for. Connection is blocked, with an explanation, when secure storage is unavailable.
1.0.0 claims macOS only. The suite and desktop compilation pass on macOS and in an ARM64 Linux JDK 21 container, and a live Secret Service save/read/delete smoke test passes headless, but passing in a container is not the same as someone running the desktop app. Windows runtime verification, Windows and Linux notifications, and .msi/.deb packaging are deferred to 1.1.0 rather than dropped — the code stays cross-platform, only the promise is narrowed. The CI run that would cover all three is blocked by GitHub Actions billing.
On this Mac, Homebrew installed JDK 21 at /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home. If your shell still selects another JDK, set JAVA_HOME to that path before running the commands. Open the root directory as a Gradle project in IntelliJ IDEA and select JDK 21 as the Gradle JVM. The build requests a JDK 21 toolchain.
Packaging needs a different JDK. test and run are happy with Homebrew's build, but Compose refuses to run packageDmg against it, because jpackage produces broken bundles from that distribution (compose-multiplatform#3107). Point JAVA_HOME at any non-Homebrew JDK 21 — Microsoft, Temurin and Corretto builds all work — before running:
JAVA_HOME=/path/to/non-homebrew-jdk-21 ./gradlew :app-desktop:packageDmg
The result lands in app-desktop/build/compose/binaries/main/dmg/. Setting compose.desktop.packaging.checkJdkVendor=false silences the refusal but not the underlying problem, so it is not the way round this.
CreditWatch is a Kotlin/JVM desktop application. It reads Vast.ai through the provider API using a locally stored key. The macOS .dmg builds and installs; Windows .msi and Linux .deb are configured but untested. npm and Bun packages are not part of the product.
1.0.0 monitors one provider account. The settings pane lists other providers and marks them unavailable, because only Vast.ai has an adapter. The storage schema and the burn engine are already keyed by account, but the session and the popover hold one of everything, so a second account is a 1.1.0 change rather than a configuration away.
The planned app version is set once in gradle.properties. Roadmap tracks release gates, changelog records shipped changes, and development flow defines issue, pull request, and release checks. GitHub Actions is configured to test and compile on macOS, Windows, and Linux for pull requests and changes to main. There is no automatic deployment or installer publication yet.
| Module | Responsibility |
|---|---|
core-domain | Provider-neutral values and snapshots |
provider-api | Read-only provider contract |
provider-vast | Vast.ai client and mapping |
core-engine | Burn, runway, alert, and efficiency calculations |
persistence | SQLite schema, migrations, repositories |
notifications | Desktop notifications and deduplication |
telemetry-api | Optional telemetry contract |
telemetry-agent | Independently runnable Linux agent, later |
app-desktop | Compose UI and application wiring |
test-fixtures | Provider response fixtures |
The app is the composition root. The core modules must not depend on Compose, HTTP, SQLite, or Vast.ai. Provider DTOs stay inside provider-vast.
The first vertical slice is API key → Vast account and instances → known burn → planning runway → three cards. The adapter currently uses the Vast OpenAPI specification for /api/v0/users/current and paginated /api/v1/instances. Missing compute or storage prices make runway unavailable; bandwidth is displayed as excluded.
Balances use decimal money, not floating point. Rates normalize to hourly values. Unknown bandwidth cost stays unknown; a known partial burn is never shown as an exact total. A stopped Vast instance may still incur storage charges. Safe runway uses the greater of current burn and the available one-hour moving average, multiplied by a safety factor of 1.10. History gaps do not count as zero burn. The factor is fixed for now; it will become configurable in settings. Runway excludes unprojected bandwidth and is an estimate, not a guarantee.
The full product specification was supplied in the project discussion. This README captures the product boundary and starting architecture; detailed alert thresholds, telemetry transport, persistence schema, acceptance cases, and release criteria will be implemented in their respective milestones.
29 commits
Kotlin
90.2%
Shell
6.2%
Java
3.6%
Local-first macOS menu bar app that tells you how long your prepaid cloud GPU credit will last. Reads Vast.ai, computes burn and runway, warns before you run dry.
Kotlin
0
29 commits
updated Sep 21, 2026
CreditWatch is a local-first desktop utility for monitoring prepaid cloud credits. It shows your balance, known burn rate, estimated runway, and obvious signs of underused resources in a compact menu bar popover. Vast.ai is the first provider.
Status: 1.0.0, released 21 September 2026 — macOS, one Vast.ai account.
Connect a Vast.ai key and CreditWatch holds it in Keychain, refreshes every 60 seconds with backoff and stale marking, and answers one question from the menu bar: how long does the credit last at the current burn. Readings are kept locally in SQLite for 72 hours, and the one-hour burn average feeds a safe runway that carries a 10% buffer. Low-runway alerts fire at 12h, 6h and 1h with persisted deduplication — each threshold switches off individually, and one the runway has already fallen under cannot be armed, because it would report the past instead of warning early. Alerts can also reach a phone through an ntfy topic paired by QR code, while the app is running.
What 1.0.0 does not do. One provider account at a time; the settings pane lists the others and marks them unavailable. Phone alerts need the app awake, since a sleeping computer measures nothing. macOS is the only platform claimed — the Windows and Linux credential stores are written and unit tested, and the Linux Secret Service round trip passes headless, but no one has run the desktop there. Bandwidth is treated as an unknown cost and left out of burn. Stopped-instance behaviour against a live account is still unverified.
Next, in 1.1.0: several providers at once, Windows and Linux as claimed platforms, burn-spike alerts, and the telemetry agent. See the roadmap.

The popover is the whole application — there is no main window. It opens under the menu bar icon and closes when it loses focus. The account above is a real one that has reached zero, which is the state most worth showing, and every part of it is readable:
0m is not used for a depleted account, because a
countdown at zero still looks like a countdown.SAFE RUNWAY repeat the state in a
second channel for anyone who cannot rely on the colour.
Settings slides in over the popover rather than opening a window. Providers are rows, so a second account is an obvious next step rather than a redesign. Only Vast.ai has an adapter today and only it can be connected; the rest are listed and visibly unavailable, because a user who came for RunPod should learn that here rather than by hunting for it. Each unavailable row carries a hover hint explaining what is missing. Monitoring still runs a single account — the roadmap names the four things a second one needs.

alerted when that threshold is the one currently
tripped, and held when it cannot be armed — a threshold the runway has already fallen under
is refused, because switching it on would fire at once instead of warning early. A threshold
armed before the fall still fires; the reading governs arming only.Kotlin/JVM and Java 21, Gradle, Compose Multiplatform Desktop, Ktor Client, Kotlin coroutines, and SQLite/JDBC. Java source can live beside Kotlin source in each module under src/main/java. Dependencies for later milestones will be added when their implementation begins.
Install JDK 21, then from the repository root:
./gradlew test
./gradlew :app-desktop:run
Look for the CreditWatch pulse icon near the right end of the menu bar. Click it for the popover, positioned beneath the icon; the tray menu shows it, refreshes, opens settings, cycles the theme, or quits. Settings slides in over the popover rather than opening a window: providers, the runway alert switches, phone pairing, and appearance as a single icon that cycles System, Dark and Light. The provider connection screen links to Vast's API key guide. Where no system tray is available the popover opens directly as a plain window, and closing it quits.
The desktop app uses Kotlin/JVM and Compose Desktop on JDK 21. The application code targets macOS, Windows and Linux, and secure key storage uses the native service on each: Keychain, Credential Manager, or Secret Service through the secret-tool executable from libsecret tools, which Linux also needs an unlocked keyring for. Connection is blocked, with an explanation, when secure storage is unavailable.
1.0.0 claims macOS only. The suite and desktop compilation pass on macOS and in an ARM64 Linux JDK 21 container, and a live Secret Service save/read/delete smoke test passes headless, but passing in a container is not the same as someone running the desktop app. Windows runtime verification, Windows and Linux notifications, and .msi/.deb packaging are deferred to 1.1.0 rather than dropped — the code stays cross-platform, only the promise is narrowed. The CI run that would cover all three is blocked by GitHub Actions billing.
On this Mac, Homebrew installed JDK 21 at /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home. If your shell still selects another JDK, set JAVA_HOME to that path before running the commands. Open the root directory as a Gradle project in IntelliJ IDEA and select JDK 21 as the Gradle JVM. The build requests a JDK 21 toolchain.
Packaging needs a different JDK. test and run are happy with Homebrew's build, but Compose refuses to run packageDmg against it, because jpackage produces broken bundles from that distribution (compose-multiplatform#3107). Point JAVA_HOME at any non-Homebrew JDK 21 — Microsoft, Temurin and Corretto builds all work — before running:
JAVA_HOME=/path/to/non-homebrew-jdk-21 ./gradlew :app-desktop:packageDmg
The result lands in app-desktop/build/compose/binaries/main/dmg/. Setting compose.desktop.packaging.checkJdkVendor=false silences the refusal but not the underlying problem, so it is not the way round this.
CreditWatch is a Kotlin/JVM desktop application. It reads Vast.ai through the provider API using a locally stored key. The macOS .dmg builds and installs; Windows .msi and Linux .deb are configured but untested. npm and Bun packages are not part of the product.
1.0.0 monitors one provider account. The settings pane lists other providers and marks them unavailable, because only Vast.ai has an adapter. The storage schema and the burn engine are already keyed by account, but the session and the popover hold one of everything, so a second account is a 1.1.0 change rather than a configuration away.
The planned app version is set once in gradle.properties. Roadmap tracks release gates, changelog records shipped changes, and development flow defines issue, pull request, and release checks. GitHub Actions is configured to test and compile on macOS, Windows, and Linux for pull requests and changes to main. There is no automatic deployment or installer publication yet.
| Module | Responsibility |
|---|---|
core-domain | Provider-neutral values and snapshots |
provider-api | Read-only provider contract |
provider-vast | Vast.ai client and mapping |
core-engine | Burn, runway, alert, and efficiency calculations |
persistence | SQLite schema, migrations, repositories |
notifications | Desktop notifications and deduplication |
telemetry-api | Optional telemetry contract |
telemetry-agent | Independently runnable Linux agent, later |
app-desktop | Compose UI and application wiring |
test-fixtures | Provider response fixtures |
The app is the composition root. The core modules must not depend on Compose, HTTP, SQLite, or Vast.ai. Provider DTOs stay inside provider-vast.
The first vertical slice is API key → Vast account and instances → known burn → planning runway → three cards. The adapter currently uses the Vast OpenAPI specification for /api/v0/users/current and paginated /api/v1/instances. Missing compute or storage prices make runway unavailable; bandwidth is displayed as excluded.
Balances use decimal money, not floating point. Rates normalize to hourly values. Unknown bandwidth cost stays unknown; a known partial burn is never shown as an exact total. A stopped Vast instance may still incur storage charges. Safe runway uses the greater of current burn and the available one-hour moving average, multiplied by a safety factor of 1.10. History gaps do not count as zero burn. The factor is fixed for now; it will become configurable in settings. Runway excludes unprojected bandwidth and is an estimate, not a guarantee.
The full product specification was supplied in the project discussion. This README captures the product boundary and starting architecture; detailed alert thresholds, telemetry transport, persistence schema, acceptance cases, and release criteria will be implemented in their respective milestones.
29 commits
Kotlin
90.2%
Shell
6.2%
Java
3.6%