PPQ Plugin Design and Implementation Plan

Core-owned architecture is documented in Model sources and managed OpenCode runtime. The runtime/configuration sections retained below record the original PPQ implementation history; the core document describes current shared ownership. Payments use the core NWC payment service, and registration follows capability services.

Status: local v0.1.0 release candidate; remote publication verification pending

Concurrent-runtime revision (Core 13.1 / Job 4.7)

The exclusive-mode and source-switch restart decisions below describe the initial PPQ implementation. The current direction supersedes them for inference routing:

The remaining sections retain the original implementation history and PPQ funding, proxy, and security requirements; their exclusive-source and global-drain language is superseded by this revision.

Concurrent-runtime implementation checks:

Goal

Add a workspace-scoped ppq app that makes PayPerQ the exclusive OpenCode model source while active. The app manages PPQ accounts and API keys, exposes the PPQ model catalog in AppWeaver, routes inference through a locally managed privacy proxy, and supports user-approved Lightning top-ups through a selected core NWC connection.

The work should also establish reusable model-source infrastructure so Routstr can later move out of core without copying PPQ-specific account and payment behavior.

Confirmed Product Decisions

Scope and activation

Managed OpenCode configuration

Restart behavior

Accounts and keys

Secret storage

NWC and top-ups

Balance checks and reports

Money presentation

Models and picker

Catalog refresh

Private-mode proxy

Capability strategy

Explicitly deferred

Current-System Gaps This Work Must Address

  1. Plugins are statically registered and have no unload lifecycle. PPQ therefore needs an internal workspace active mode, as decided.
  2. Parent opencode.json currently symlinks to the AppWeaver repository config, which conflicts with independent workspace state.
  3. The AppWeaver root opencode.json is tracked, so adding it to .gitignore alone cannot prevent diffs.
  4. OpenCode config is loaded at startup. Cache invalidation alone cannot apply a provider/model change.
  5. There is no central drain/restart coordinator covering WebSocket, Nostr, plugin, HTTP, and background OpenCode runs.
  6. The composer provider list is hard-coded to local and routstr, and model selection is tied to core commands rather than a model-source contract.
  7. Plugin APIs can request a run-scoped model but cannot safely contribute a persistent active model source or managed OpenCode configuration.
  8. Existing interactive payments are request-scoped and WebSocket-oriented. DM/push-approved payments need persisted intents and a core-authorized NWC execution path.
  9. Payment attempts and settlement reconciliation are in memory. Restart-safe top-up and resume require persisted state and idempotency.
  10. Existing NWC URIs are stored plaintext in core state.
  11. Routstr's current balance is not an enforced run budget, its money units are inconsistent in places, and its recovery paths are not safe templates for PPQ.
  12. OpenCode already exposes assistant-message cost and detailed token metadata, but AppWeaver's run result currently discards reasoning/cache token details. PPQ phase one only needs session cost, not new charge reconciliation.

Proposed Architecture

Composer / core agent flow
          |
          v
  active model-source resolver
     |                 |
     | normal          | PPQ active
     v                 v
 core OpenCode     ppq plugin capability
 model source       | catalog / selection / preflight
                    | accounts / incidents / settings
                    v
             workspace PPQ database
                    |
                    v
          managed loopback PPQ proxy
                    |
          +---------+----------+
          |                    |
    Tinfoil path          Nitro path
    private/*             ordinary models

PPQ top-up approval
          |
          v
 persisted PPQ payment intent
          |
          v
 core payment authorization service
          |
          v
 selected encrypted core NWC connection
          |
          v
 PPQ invoice status / balance reconciliation
          |
          v
 resume same OpenCode session

Core responsibilities

PPQ plugin responsibilities

Model-Source Capability v1

Recommended contract name: ai-model-source, version 1.

The exact schemas should follow the existing capability conventions in src/capabilities/types.ts. Proposed operations are:

capability:v1:ai-model-source.get-state
capability:v1:ai-model-source.list-models
capability:v1:ai-model-source.select-model
capability:v1:ai-model-source.set-favorite
capability:v1:ai-model-source.record-use
capability:v1:ai-model-source.preflight
capability:v1:ai-model-source.get-runtime-config
capability:v1:ai-model-source.get-context-usage

Recommended state fields:

sourceId
title
active
transitionState
health
selectedModelId
catalogFetchedAt
catalogStale
settingsAction
statusAction

Recommended model fields:

id
providerModelId
name
ownedBy
type
popular
privacyLevel
contextLength
inputModalities
outputModalities
supportedParameters
pricing
favorite
lastUsedAt
availability
unavailableReason

get-runtime-config must return only a schema-validated, secret-free config contribution. Runtime secrets are injected by trusted process management, not returned through the capability registry.

get-context-usage takes the active workspace and OpenCode session ID and returns nullable session token usage with a nullable model context limit and percentage. Each source owns the limit for its active model: Core uses OpenCode provider metadata, while PPQ uses its validated model catalog. PPQ streaming responses can leave OpenCode's reported token counts at zero despite a completed turn; in that case show an explicitly labeled estimate from available session text, rather than silently hiding context or presenting the estimate as exact. No usage data is reported before the session has assistant messages.

Because core itself consumes this capability, the registry needs a host caller path in addition to the current plugin-scoped caller path. Host calls must still perform provider selection and input/output validation. PPQ activation makes it the explicit active provider; multiple registered providers must not rely on the existing single-provider auto-selection behavior.

Managed OpenCode Config Design

Files

For either active workspace root <workspace>:

<workspace>/.appweaver/opencode.json       canonical normal config
<workspace>/opencode.json                  generated active runtime config
<workspace>/.appweaver/ppq/db.sqlite       PPQ workspace state

Recommended tracked bootstrap template:

templates/opencode/opencode.json

The exact template path may follow existing repository conventions, but it must not be confused with generated output.

Normal mode

Materialize the canonical .appweaver/opencode.json as the root runtime configuration after schema validation and normalization.

PPQ mode

Generate a PPQ runtime variant without modifying the canonical normal config:

Migration

  1. Resolve both AppWeaver and parent workspace roots.
  2. Detect regular files versus the current parent symlink.
  3. Import each workspace's effective existing config into its canonical .appweaver/opencode.json without overwriting an existing canonical file.
  4. Preserve the repository default as a tracked template.
  5. Remove the parent-shared-config behavior from setup and workspace assets.
  6. Add generated root config and workspace PPQ runtime data to the appropriate .gitignore files.
  7. Stop tracking the repository root runtime output as part of the migration.
  8. Materialize and validate normal mode before allowing PPQ activation.

Migration must be idempotent and must not replace malformed user config with {}. Parse failure is a blocking error with a path and recovery instructions.

OpenCode Drain and Restart Controller

Introduce one core controller around all OpenCode backend entry points.

Recommended states:

running -> drain_requested -> draining -> restarting -> running
                                      \-> failed

Required behavior:

PPQ Workspace Data Model

Use a versioned SQLite schema under the active workspace. Suggested tables:

ppq_accounts

id                         local UUID primary key
label                      unique workspace label
credit_id_ciphertext       versioned NIP-44 envelope
selected_runtime_key_id    nullable local key reference
created_at
updated_at
last_balance_usd           nullable decimal string
last_balance_checked_at    nullable timestamp

ppq_api_keys

id                         local UUID primary key
account_id                 foreign key
remote_id                  nullable PPQ key object ID
name
api_key_ciphertext         versioned NIP-44 envelope
usage_limit_usd            nullable decimal string
current_period_usage_usd   nullable decimal string
total_usage_all_time_usd   nullable decimal string
reset_period               nullable daily/weekly/monthly
reset_at                   nullable timestamp
expire_at                  nullable timestamp
revoked_at                 nullable timestamp
created_at
updated_at

ppq_account_settings

account_id                 primary/foreign key
nwc_connection_id          nullable core NWC UUID
topup_presets_sats_json
low_balance_threshold_sats nullable; disabled when null

ppq_workspace_settings

singleton_id
active
selected_account_id        nullable
selected_model_id          nullable
recent_model_limit         default 5
notify_push                default true
notify_dm                  default true
catalog_ttl_seconds

ppq_models

Store normalized validated fields plus optionally bounded raw JSON for forward compatibility. Include API order and cache generation so stale rows can be atomically replaced.

ppq_favorites and ppq_recent_models

Favorites are a set keyed by model ID. Recent models retain selection/use time and are pruned to the configured MRU limit.

ppq_funding_incidents

Persist workspace, account, OpenCode session ID, model, state, triggering error, known session cost, safe report data, notification delivery IDs, and resume state. Do not retain top-up invoices as incident data.

Do not store raw prompts merely to build a report. Read session history when authorized and persist only the minimal report snapshot needed for restart-safe delivery and action handling.

On-Demand Top-Up Flow

The authenticated Web widget requests a fresh Lightning invoice for the chosen satoshi amount. Core's interactive payment broker validates its amount, hash, network, and expiry, then presents the invoice for explicit user approval. The broker verifies NWC preimages and checks PPQ settlement before claiming success. The invoice and payment result exist only during this request; closing the modal or restarting AppWeaver does not leave a local PPQ payment attempt to recover. If payment status is ambiguous, direct the user to PPQ account activity before another manual payment. Never create and pay a replacement automatically.

The future 402 incident flow persists incident and resume state separately; it must not introduce a PPQ invoice-history table as part of ordinary top-ups.

402 Incident and Resume Flow

  1. A PPQ-backed OpenCode turn returns an error classified as insufficient balance.
  2. Core/plugin captures the session ID, model, workspace, selected account, and safe error metadata.
  3. The PPQ app reads existing OpenCode session messages and computes known session cost from assistant-message metadata.
  4. It builds a deterministic report from the original task label/excerpt, completed steps/tool results, partial assistant output, file-change summary, last activity, and error. Report generation must not call PPQ.
  5. A persisted incident is created before notifications are sent.
  6. Minimal push and detailed encrypted DM are sent according to settings.
  7. The user selects a preset/custom sat amount.
  8. PPQ creates a Lightning top-up invoice for that exact requested amount.
  9. The user reviews the parsed invoice and approves Top up and resume, Top up only, or cancels.
  10. Core pays through the account's assigned NWC connection.
  11. PPQ credit is reconciled.
  12. For Top up and resume, AppWeaver reuses the same session ID and sends a visible generated continuation instruction that funding was restored and the interrupted task should continue from current state.

The continuation instruction must tell the model to inspect existing state and avoid repeating completed side effects. This reduces duplicate work but cannot guarantee idempotency for arbitrary external tools; the report and UI should state that limitation.

Commands and Widget

Recommended command surface:

/ppq                         open PPQ overview widget
/ppq enable
/ppq disable
/ppq status
/ppq models
/ppq accounts
/ppq keys
/ppq topup
/ppq incidents
/ppq settings
/ppq proxy

Generated command definitions should provide forms for create/import account, key creation/editing, account selection, NWC assignment, top-up amount, and settings. The primary Web experience is one PPQ widget with sections:

The PPQ widget must warn rather than render unusable actions when account, runtime key, NWC assignment, catalog, proxy, or balance prerequisites are missing.

Security Requirements

Before implementing live flows, capture redacted fixtures or confirm exact schemas for:

Questions that implementation must answer from real responses rather than guessing:

Implementation Phases

Checked items are implemented in this worktree. They do not imply that live inference, paid flows, or the manual end-to-end matrix have been verified. Remaining portions of mixed items are listed separately below.

Current implementation: core secret storage, managed OpenCode configuration, runtime transitions, model-source picker, local PPQ credentials/catalog, automatic vendored-proxy build, activation, and key-authenticated balance preflight. Composer session context usage is supplied by the active model source, with PPQ's limit from its validated catalog. Selected PPQ workspaces restore the proxy and model source in the background after bot startup; PPQ runs wait for readiness. The independent plugin repository is prepared for a local v0.1.0 release so clean-install and funded-flow verification can use an immutable revision. Still outstanding: remote account/key management, live PPQ top-up response verification and cross-transport funding approval, 402 incident reporting and resume, live standard/private inference validation, and release hardening. A web-initiated Lightning top-up uses the core interactive payment broker without persisting invoice attempts.

Phase 0: Contract and runtime spikes

Exit criteria:

Phase 1: Secret-storage hardening

Exit criteria:

Phase 2: Managed OpenCode configuration

Exit criteria:

Phase 3: OpenCode drain/restart controller

Exit criteria:

Phase 4: Model-source capability and composer integration

Exit criteria:

Phase 5: PPQ plugin foundation

Exit criteria:

Phase 6: Vendored proxy and PPQ activation

Exit criteria:

Phase 7: On-demand Lightning top-ups

Exit criteria:

Phase 8: 402 reporting and session resume

Exit criteria:

Phase 9: Hardening and release

Test Matrix

Unit

Integration with mocks

Manual end to end

These are implementation recommendations, not additional settled product requirements:

  1. Use a vendored source snapshot plus an UPSTREAM.md file containing the repository URL, commit, version, license, local patches, and update steps. Avoid both a nested clone and Git subtree unless repeated manual syncing becomes burdensome.
  2. Keep the default top-up presets at 100, 1k, and 5k sats initially and revise after real usage.
  3. Queue new runs during a short OpenCode drain rather than rejecting them, but expose the waiting state clearly.
  4. Use one generated continuation message after top-up and keep it visible in session history for auditability.
  5. Store USD values as validated decimal strings in PPQ state. OpenCode config may require numeric prices, but that conversion should happen only when generating the model map.
  6. Keep detailed funding reports available in the authenticated widget and use notification payloads only as projections of persisted incident state.
  7. Bind the managed OpenCode server to loopback while introducing the proxy, unless a separately documented external server mode requires otherwise.
  8. Add retention settings for resolved incidents after observing database growth. Do not add top-up invoice records or raw prompt retention.
  9. Treat private-proxy runtime compatibility as a phase-0 gate. Vendoring source does not remove its Node 20+ and dependency requirements.

Known Risks

Definition of Done