# SDK parity and validation

Contract snapshot: August 1, 2026. Live timings and service observations below are dated July 2026 samples rather than current service guarantees.

This document records the product boundary and measured evidence. It is not a claim that terminal presentation replaces native UI testing. The repository root `totest.md` is the only queue for outstanding manual, live-service, browser, App Store-style, and physical-device verification.

## Shared-owner boundary

### disposable lifecycle check — september 14, 2026

seven temporary production-realm accounts used REGTEST bitcoin only. account/vault creation, encrypted group text, photo and audio delivery/decryption, reactions, and confirmed peer transfers worked. call media and participants reached readiness; a complete live call and native screenshot capture were not performed. both temporary groups and all seven accounts were subsequently deleted through ordinary sdk commands. an independent read verified that all seven Firebase Auth users and public profiles were absent, and the temporary local profiles/cache were removed.

the run exposed and corrected four lifecycle boundaries: account deletion must scan persistent inbox slots once rather than wait for them to disappear; byte-identical vault snapshots must retain the same authority during password verification; deletion must wait through listener reconnects before remote phases; and stable-id sdk chat commands must hydrate the private owner instead of acting on a cached list row. pending message requests remain admission decisions and do not require an already-admitted owner. cleanup now reconciles each discovered chat's membership before choosing its retirement action, with phase diagnostics that omit private data.

the older group's missed-transition failure exposed a rules defect: membership changes add retirement metadata to retained encrypted settings, but reads accepted only the active record shape. the firestore emulator reproduced a denied settings read after an actual membership/media transition; the corrected rule passes that regression while preserving immutability, strict active writes, and denial after chat deletion. production firestore rules were deployed with `bun make db`. the original accounts were already deleted, so a fresh live multi-transition catch-up remains in `totest.md`; successful cleanup does not prove that full scenario. the sdk/chat suite passes 296 tests, the firestore suite passes six, and focused lint and sdk packaging pass. the run procedure and identity rules live in [bot contracts](../../../guidelines/bots.md#traffic-verification).

The Node SDK composes the same framework-free owners as web and iOS:

| Product area | Shared owner used by the SDK | Node adaptation |
| --- | --- | --- |
| Auth/session | `core/account/session.js`, `core/account/user.js` | local machine credential or browser-assisted passkey |
| Vault | `core/vault.js`, shared crypto/seed owners | local vault key or caller secret manager |
| Peers/search/block | `core/peerlist.js`, `core/peers.js`, shared search sessions | JSON peer projections |
| Chat | `core/chat/session.js`, `core/chat/actions/*`, shared message batches/cache | strings, byte buffers, filesystem paths, JSON events |
| Wallet/transactions | `core/wallet/session.js`, `core/wallet/invoice.js`, transfer store, tx data | direct invoice/address strings, integer sats, and JSON results |
| Public invites | `core/invite.js` | direct Veyl URL strings |
| Passkeys | shared passkey eligibility and normal backend link/passkey owners | browser URL for WebAuthn verification |
| Settings/cache | shared settings normalization and vault-encrypted cache | JSON patches and byte counts |
| Support | shared ticket, report, and evidence builders | terminal text and existing message attachment |

The obsolete direct-cloud `createAccountSessionActions` implementation was removed during this pass. There is now one live product path: the SDK adapts inputs around the same chat, wallet, peer, user, vault, cache, search, and cloud owners used by the interactive clients.

`src/client.js` now requires explicit runtime ports and imports no Node built-ins, Firebase/Spark constructors, filesystem storage, machine-key implementation, namespace file, or browser process. `src/runtime/node.js` is the single current provider of those capabilities. Boundary tests enforce that dependency direction, while construction tests prove callers cannot instantiate the private account runtime without a complete platform boundary.

CLI paths and persistent-session dispatch come from `src/commands.js`. Both one-shot and persistent CLI work invoke the same account runtime returned by `open()`. The command registry remains an adapter detail; the package root exposes only `startSessionRuntime` so an embedding agent can serve its existing unlocked client through that owner-only socket and stop it with an abort signal.

The canonical registry defines 78 CLI commands, 70 persistent-session product commands, and 16 command groups. Wallet mnemonic export and passkey-link creation stay one-shot CLI/JavaScript API only; the namespace key, session owner, and documentation resource process controls also remain CLI-only. Registry coverage checks map every enabled session command and real CLI path to its declared public client method, with `chat.reactTo` retained only as a JavaScript convenience alias over the same reaction operation.

Focused contract coverage mounts the real framework-free chat route through the public SDK runtime, verifies live projection, explicit leave, and manual/disabled read policy, and exercises persistent-session streaming plus abort-driven embedding shutdown. Agent-listener coverage proves a changed conversation uses a transient message window and releases it instead of retaining one listener per loaded chat, including a 500-chat requested window. Replay coverage expands older pages until a per-chat checkpoint is found, then emits only the newer messages. Chat-admission coverage uses real MLS Welcomes to prove a frozen public callback boundary, block precedence, fail-closed decisions, prekey cleanup, and ongoing enforcement for established chats without automatic delete or leave events. The packaged single-agent example proves pinned owner filtering, default Node live transport, stable typing-to-message composition, deterministic final delivery, app-server JSONL/backpressure/errors, steering, owner-only state, and completed-turn recovery. Fleet coverage opens fake public clients under bounded concurrency, tags their events, runs policies, and proves all account and policy lifecycles close. Root coverage proves stable isolated account derivation, monotonically indexed recovery, and owner-only seed storage; manifest and profile coverage rejects or omits per-account secrets. Namespace coverage proves the local private seed is mode `0600`, never returned by initialization, and produces a claim that verifies only for its exact username, machine credential, and expiry; it also keeps the generated Functions trust anchor aligned with the shared public key. Local-owner coverage proves a second healthy process is never replaced and a dead same-host PID lock can be reclaimed without signaling it. The bounded event checkpoint persists processed IDs and baseline policy coverage proves historical messages are recorded without bot behavior. Action-journal coverage proves completed effects survive restart, replay-safe chat actions can reuse their deterministic CID, interrupted money actions stop for reconciliation, and a replayed faucet request is paid and mirrored once. The canonical Veyl-managed runtime now consumes the packaged fleet example through ordinary SDK clients; remaining live fleet observations belong only in the root manual queue.

Run `bun check:sdk` from the repository root to validate the exact public package artifact without mutating the intentionally versionless source manifest. The check builds the SDK, stamps the root product version into a temporary copy, dry-run packs it, verifies the required public runtime entries, documentation, fleet example, and single-account connector, and rejects private source or tests.

API version 4 keeps `open()` as the public SDK composition root and adds stable-id chat/group actions without exposing private owners. The root now also exposes the abortable `startSessionRuntime` embedding boundary; the owner-whitelist policy remains local to the single-agent example, while action-executing MCP and the command registry remain private. API version 2 added the public root-derived fleet owner, durable action journal, and narrow ambiguous-money-outcome contract. Focused tests prove non-idempotent Spark failures are not marked retryable, Lightning failures are retryable only with the same caller idempotency key, caller-owned chat CIDs and composition ids reach the shared send owner, and structured error fields survive the persistent-session transport.

## Canonical command coverage

| Group | CLI / session | Product owner used by the client |
| --- | ---: | --- |
| Namespace | 1 / 0 | owner-only local Ed25519 seed and portable reserved-name claim; intentionally not a persistent-session or general client capability |
| Account | 8 / 8 | `core/account/user.js`, `core/account/session.js`; machine-auth transport in `functions/machineauth/account.js` |
| Vault | 4 / 3 | `core/vault.js`, shared vault-seed crypto, and `core/account/session.js`; mnemonic export is not callable through the persistent session |
| Profile | 3 / 3 | shared user owner and Firebase cloud adapter |
| Settings | 2 / 2 | shared user settings owner and normalization |
| Cache | 2 / 2 | shared vault-encrypted local-data cache |
| Peers | 6 / 6 | `core/peerlist.js`, `core/peers.js`, and shared search sessions |
| Chat | 44 / 44 | `core/chat/session.js`, stable-id notes/direct/group creation, membership epochs, mounted routes, shared actions, history, retention, and attachment owners |
| Wallet | 10 / 10 | `core/wallet/session.js`, claim, transfer-store, and tx-data owners |
| Lightning | 5 / 5 | the same shared wallet session and Spark request owners |
| Withdrawal | 3 / 3 | shared wallet session, fee normalization, and generic payment-intent/L1 review owner |
| Invite | 2 / 2 | `core/invite.js`; direct URL strings only |
| Passkeys | 3 / 2 | shared passkey eligibility plus the normal cloud passkey owners; bearer-link creation is one-shot only and WebAuthn stays browser-assisted |
| Support | 2 / 2 | shared ticket, report, message lookup, and evidence builders |
| Session | 4 / 0 | local process/socket lifecycle around the same account runtime |
| Docs | 1 / 0 | local stdio MCP exposing allowlisted packaged Markdown resources only |

## Major capability matrix

| Capability | CLI/API | Live edge evidence |
| --- | --- | --- |
| Machine account create/login/logout/agreement | yes | two full disposable accounts created and repeatedly cold/warm authenticated; current agreement is recorded at creation and existing accounts can inspect/accept future revisions through the shared user owner |
| Passkey account create/login | yes, browser-assisted | flow and registry present; passkey link lifecycle exercised |
| Logout this/all sessions | yes | warm revoke tears down local state; delayed cached snapshots no longer sign out a fresh login |
| Account delete | yes | disposable accounts deleted with empty/non-empty chat state and a 5-sat REGTEST balance; local credentials disappeared, privileged reuse failed, and a final filesystem audit proved the complete encrypted cache scope is removed |
| Vault create/unlock/lock/export | yes | create/unlock/lock and secret export shape exercised without retaining output |
| Profile/avatar | yes | profile reads work; avatar set/clear uses the authenticated server publish boundary and exact immutable cache version |
| Settings/cache | yes | encrypted settings update and cache show/clear exercised |
| Peer lookup/search/list/block | yes | self-block rejected; block projection and list state corrected; chat hides/restores |
| Text, reply, edit, reaction, save, delete, retention | yes | mutation ordering, delete/send races, explicit delete, retention behavior, and count-driven older-history paging exercised |
| Attachment send/multi-send/share/download | yes | same encrypted media owners exposed; remaining cross-client and live Storage checks are in the root manual queue |
| Live chat/transaction events | yes | multiple subscribers share one message source, remain live across repeated batches, and unsubscribe independently |
| Wallet balance/address/claim/send/request/pay | yes | direct Spark send and recipient convergence exercised; request edge validation exercised |
| History/detail/search | yes | paging/detail and `@peer` shared transaction search exercised |
| Lightning invoice/quote/pay/status | yes | fixed and variable paths plus idempotent repeat exercised |
| Withdrawal quote/prepare/confirm | yes | a live 3,000-sat cooperative exit paid a 2,190-sat medium fee and funded the receiver's static address with 810 sats; the run exposed and fixed CLI re-quoting at confirm, and post-fee dust now rejects before spend |
| Invite create/read | yes | all four shared invite kinds round-trip as direct URL strings; invalid kinds and amounts reject |
| Passkey list/link/delete | yes | pending link create/delete exercised; list marks current credential and shared deletion eligibility |
| Tickets/reports | yes | report-without-evidence succeeded live; ticket submission and reporting share one SDK account owner and remaining cross-client checks are in the root manual queue |
| Docs discovery | yes | focused tests prove the stdio MCP advertises only documentation resources, reads only allowlisted packaged Markdown, and rejects action tool calls without loading a Veyl client |

## Edge cases exercised

- Concurrent same-chat sends are FIFO; disjoint chats may progress concurrently.
- Failed text and attachment sends retry through the shared chat owner with their original CID; an absent/non-failed CID rejects instead of pretending to retry.
- Whole-chat delete is exclusive with sends. A delete-first race lets a later send retry once against the new active generation; a send-first race lets delete win afterward.
- Deleted-chat tombstones prevent stale owner/list snapshots from resurrecting a locally deleted chat.
- Multiple event subscribers share the underlying message-batch source lease.
- Concurrent chat-list publications cannot attach the same event source twice and accidentally release its only lease; two subscribers each received two successive live messages exactly once.
- SDK history and event projection use the same retention and reaction derivation primitives as route UI, including reaction removal updates to an already-visible message.
- Explicit message deletion emits immediate removal; retention/source expiry remains held for an already-open route until release.
- Self-block is rejected, blocked state is reflected in returned/listed peer projections, and blocked chats are hidden.
- Blocking retires the known opaque generation. If a sender writes into a newly issued generation while delivery remains blocked, the recipient's encrypted first-accepted timestamp floors every later listener, history page, cleanup scan, and direct lookup; the blocked-period record did not appear after unblocking and was rejected by targeted save lookup.
- A revoked local session stops privileged work without hanging on a post-revocation presence write.
- Cached Firestore auth snapshots are not treated as authoritative revocation state, so delayed cache delivery cannot kill a newly authenticated generation.
- Logout-all failure leaves the current session usable; a successful logout-all invalidates it.
- Account deletion fails closed if encrypted chat discovery or vault proof fails, and removes the local profile only after backend success.
- Backend account-deletion failure leaves the live session, encrypted cache, and local profile intact; successful deletion destroys that private cache only after the backend commit.
- Wallet request payment rejects own, already-paid, missing, and wrong-network requests through the shared wallet/chat owners.
- Lightning idempotency keys return the existing payment rather than creating a second spend.
- Ambiguous money mutations expose `operation_outcome_unknown` with the caller operation id. Spark sends, request/invoice payments, and withdrawals are non-retryable until reconciled; Lightning is retryable only with the exact same idempotency key.
- Invalid withdrawal input fails before wallet mutation.
- A prepared withdrawal rejects when its post-fee output falls below the destination script's dust floor; the confirmed P2TR boundary is 330 sats.
- The live CLI confirmation exposed that reviewed fee flags were not entering the command input, so it obtained a second quote; the fee happened to remain 2,190 sats. CLI confirmation now accepts the reviewed quote id and fee amount as one pair, a partial pair rejects before wallet work, and focused dispatch coverage proves the complete pair reaches the shared Bitcoin payment mutation.
- Static funding uses one identity-scoped confirmed-UTXO source. The former catch-all address-query fallback was removed so an upstream failure cannot double requests or silently change the privacy shape.
- A live 810-sat static funding output was claimed once for 711 sats after the 99-sat claim fee. A second explicit claim returned false and history contained one completed funding row, proving idempotent convergence rather than a duplicate claim.
- The receiver completed its funding claim before the sender's cooperative-exit transfer reached completed status. The existing outgoing reconciler stopped after two unresolved checks, so the sender row stayed pending until an explicit refresh. Cooperative exits now retain a bounded 15-second exact-transfer check for up to ten minutes; ordinary direct transfers keep the earlier low-cost stop policy.
- Direct Lightning/Spark strings use the shared wallet invoice parser; profile strings, Bitcoin addresses, and Veyl QR wrapper URLs are rejected as invoices.
- An authoritative peer refresh removes a deleted peer through the shared peer owner, which also removes its stale cached chat instead of leaving an SDK-only ghost row.
- Public account projections normalize nested chat-preview timestamps, so CLI, API, and persistent-session JSON never expose Firebase timestamp objects as `{}`.
- `chat messages --count N` advances through the same shared older-history loader as the app until it covers `N` visible messages or reaches history start; mark-read and targeted message lookup remain latest/direct reads so they do not create accidental history-read cost.
- Account deletion removes the local profile, closes wallet/vault owners, drains in-flight local work, and automatically exits every current persistent runtime as soon as its credential/session watch becomes invalid.
- Account deletion drains pending cache writes and destroys the account/network cache scope, including its install secret, rather than leaving encrypted remnants after the local profile is removed.
- A send whose direct write response is ambiguous reuses its CID as the message document id and checks that exact committed document before reporting failure. The separate sealed ping may repeat in this rare case and is reconciled by its encrypted message id.
- A disposable no-evidence peer report completed through the shared report/evidence builder, and deleting its reporter account removed the UID-owned report while repairing the target aggregate. Feedback/bug share one backend input owner whose platform set includes the SDK.

The blocked-send experiment exposed a real presentation bug—a committed ciphertext could be reported failed when the later ping was rejected—but its proposed atomic callable centralized sender/recipient IDs, both chat public keys, link ID, and chat ID on every send. The release review removed that architecture. Message and encrypted owner entry now commit directly in one client batch; the separate sealed-inbox call is best effort and cannot reverse the committed result. This preserves the narrow sender/recipient metadata exception required for block checks and sender-identifying push without joining it to the chat route.

## Measured latency

These are observed samples, not service-level guarantees. Warm values use already-running unlocked sessions; cold values include the stated setup.

| Operation | Observed time |
| --- | ---: |
| Persistent session status | ~30–140 ms |
| Warm wallet balance | ~50–130 ms |
| Warm profile read | ~50–130 ms |
| Warm settings/cache read | ~50–120 ms |
| Warm chat list projection | ~40 ms |
| Warm authoritative peer lookup | ~360–420 ms |
| Warm authoritative passkey list | ~330 ms |
| Local persistent-runtime revocation shutdown | ~40 ms |
| New machine account creation | ~2.95–6.56 s |
| Vault creation plus first Spark/chat boot | ~12.54–25.62 s |
| Typical cold unlock/boot | ~13–14.5 s |
| Warm logout-all callable in backend logs | ~0.50–0.74 s |
| Cold backend logout-all sample | ~4.78 s |
| Full warm client logout-all | ~0.77–0.96 s |
| Direct REGTEST Spark send | ~1.18–1.26 s |
| Recipient balance convergence after send start | ~1.75–1.85 s |
| Message event, command start to event | ~765 ms |
| Message event, committed timestamp to event | ~337 ms |
| July 19 warm encrypted send | ~0.90–1.08 s |
| July 19 committed timestamp to message event | ~0.41–0.70 s |
| July 19 warm push callable | ~0.28–0.44 s |
| July 19 peer block / unblock | ~1.00 s / ~0.64 s |
| Transaction event, command start to recipient event | ~3.90 s |
| Settings update | ~0.8 s |
| Cache clear | ~0.11 s |
| Lightning invoice | ~0.71 s |
| Lightning quote | ~0.36 s |
| Lightning pay | ~6.03 s |
| Idempotent Lightning repeat | ~3.98 s |
| Invalid withdrawal validation | ~69–89 ms |
| Warm withdrawal preparation / quote | ~0.16 s |
| Cooperative withdrawal confirmation before quote-reuse fix | ~8.29 s |
| L1 exit start to receiver claim completion | ~29.76 s |
| L1 exit start to Spark sender-status completion | ~63.87 s |
| Repeated post-claim no-op check | ~1.42 s |
| Warm direct Spark return transfers | ~1.84–2.20 s |
| Peer block/unblock | ~0.88–1.2 s |
| Peer report, no evidence upload | ~4.24 s |
| Account deletion, no chats/funds plus one submitted report | ~3.44 s |
| Account deletion, one chat plus 5 REGTEST sats | ~2.05 s |
| Account deletion, two non-empty chats | ~7.89 s |
| Cold account deletion plus complete local cache destruction | ~9.10 s |

Cold one-off commands are inherently inefficient for agent loops because Spark boot dominates. The persistent runtime is the intended efficient path.

## Server-operation cost shape

the repository's `costs/README.md` owns cost documentation, `costs/model.mjs` owns the partial workload calculation, and `costs/dau-scale.md` contains generated projections. packaged SDK validation deliberately does not duplicate their prices or operation coefficients. source-level behavior and dated runtime samples do not establish current billing.

The live L1 path confirmed that withdrawal, confirmed-UTXO discovery, claim, balance, and transfer status do not call a Veyl wallet Function or write wallet state to Firestore. They are client-local Spark SDK operations against Spark/network services. Active unlocked wallets currently run one identity-scoped deposit check every 20 seconds on REGTEST or every minute on MAINNET; a pending cooperative exit adds one exact-transfer Spark check at most every 15 seconds for ten minutes. Neither cadence is a Firebase cost. The sample consumed 2,190 sats for cooperative exit and 99 sats for the static claim, while all 3,711 recoverable sats were returned to the faucet.

## UI/device-only boundary

The following should remain UI/device testing rather than receive CLI-specific product paths:

- camera and QR encoding/scanning; CLI accepts direct usernames, invite URLs, addresses, and invoice strings;
- clipboard, share-sheet, file picker, and save-panel presentation; CLI accepts/returns strings, byte buffers, or paths;
- Face ID, Secure Enclave/keychain prompts, haptics, and visual privacy cloak;
- push permission prompts and native token registration;
- navigation transitions, keyboard behavior, row layout, animation, glass, avatars, and accessibility presentation.

If one of these surfaces requires different product semantics rather than input/output adaptation, the shared owner should be reconsidered before adding an SDK-only implementation.

## Remaining verification

Outstanding manual, live-service, browser, App Store-style, and physical-device checks live exclusively in the repository root `totest.md`. Do not mirror that queue here: this document owns the SDK boundary and retained evidence, while `totest.md` owns mutable validation state.
