Plan-Billed Claude Agent on Lightsail: Verification, Discovery, and the Lane-Routing Bug
This post covers the infrastructure audit and live testing of the JADA ticket-runner agent hosted on a Lightsail instance, billing to plan usage instead of API metering. We discovered a critical bug blocking agent-work tickets and verified the cross-account billing separation that keeps operational costs predictable.
Architecture Overview
The agent runs on a single Lightsail instance (jada-agent, IP 34.239.233.28) in us-west-2, executing a systemd service (jada-ticket-runner.service) that polls the progress.queenofsandiego.com site for note-driven card updates. The instance is logged into Claude as c.b.ladd@gmail.com with a plan-subscription account type (pro), ensuring all tokens bill to the subscription tier, not metered API calls.
Billing Strategy: Plan Over API
The old metered API key was already disabled in the box's .bashrc (commented as #DISABLED-PLAN-BILLING), creating a hard boundary: the runner cannot fall back to the API even if the plan account fails to authenticate. This was deliberate—to prevent accidental overspend on a heavily-used background service. We verified the setup by running a headless test:
env -u ANTHROPIC_API_KEY claude -p "say OK"
The runner responded correctly, confirming plan billing is live and the fallback is truly absent. The account separation is intentional:
- Lightsail (jada-agent): c.b.ladd@gmail.com, plan-billed, runs background agent loops
- Mac (this laptop): jadasailing@gmail.com, local plan use, no changes made
This separation means billing for the 24/7 agent loop is isolated from interactive development work, making cost tracking straightforward.
The Service Configuration
The systemd unit at /etc/systemd/system/jada-ticket-runner.service runs as an unprivileged user and loads environment variables including:
TICKET_LANE=todo— specifies which card lane to monitor (should beagent-work)AWS_PROFILE=queenofsandiego— routes AWS calls to the correct IAM rolePATH— ensures Python3 and tools are in scope for child processes
The service is enabled and active, but the live audit revealed it's pointing at the wrong lane: TICKET_LANE=todo instead of TICKET_LANE=agent-work. This means the runner never sees agent-work tickets—they queue up unprocessed.
The Lane-Routing Bug: 11 Tickets Blocked
Manual testing confirmed the fix: when the lane is overridden to agent-work, the runner fetches all 11 pending tickets (Keeli email automation, unsubscribe cleanup, tech-blog ICM scaffolding, Daniela trip sheet, Capt Darrell hours reconciliation, etc.). But in live mode, the runner processes zero because it's looking in the wrong queue.
The fix is a single edit to the systemd environment variable:
sudo sed -i "s/^Environment=TICKET_LANE=todo/Environment=TICKET_LANE=agent-work/" /etc/systemd/system/jada-ticket-runner.service
sudo systemctl daemon-reload
sudo systemctl restart jada-ticket-runner
This corrects the lane, reloads the systemd config, and restarts the service. Manual verification with systemctl is-active jada-ticket-runner confirms the service remains healthy.
Why This Matters: The Ticket-Driven Architecture
The progress site uses a note-driven workflow: a card note in the agent-work lane is the trigger for a task. The runner periodically fetches cards from the configured lane, deserializes the note payload, and executes it. The lane-based routing allows the same runner to be pointed at different queues (e.g., todo for debugging, agent-work for production) without code changes—just an environment variable edit and a restart.
This design choice trades a bit of state management (lane awareness) for flexibility: future deployments can scale to multiple runners, each watching different lanes, without duplicating logic.
Infrastructure Checklist
- Lightsail instance: active, SSH reachable, Claude plan-logged-in
- systemd service: enabled and running on the instance
- progress.queenofsandiego.com: HTTP 200 response, CloudFront live (dist ID visible in upstream config)
- AWS credentials: IAM profile
queenofsandiegoin use, no hardcoded keys in service env - Billing isolation: plan account locked in, no API key available as fallback
- Lane configuration: BROKEN — currently
todo, needsagent-work
Cross-Account Safety
A key decision: never share the plan account between local dev and the Lightsail agent. The old metered key was stored on the box and (redundantly) as a shared environment variable, but the new plan-only setup enforces separation at the OS level—no API key means no cross-cutting concern. This is why the Mac account remains untouched and unaffected by Lightsail changes.
What's Next
- Apply the lane fix — restart the runner on
agent-workto unblock the 11 queued tickets - Audit ticket latency — after the fix, measure how long it takes for a new note-trigger to execute; systemd polling interval may need tuning
- Plan for multi-instance scale — if ticket volume grows, the lane-based design allows a second runner to monitor a different lane (e.g.,
backlog) without conflict - Monitor Lightsail static IP stability — the instance is on a static public IP; confirm firewall rules and SSH key rotation policy are current
Technical Takeaways
Plan-subscription billing removes the risk of runaway metered costs on always-on infrastructure. Environment-driven configuration (lane routing) decouples deployment from code. And cross-account separation keeps billing clean: one account per role (interactive vs. background). The lane-routing bug is a simple fix—a one-line sed edit and a service restart—but it highlights the importance of live verification over assumption.
```