Overlay Capabilities and Non-Final Proposal Sessions
Different Bitcoin overlay services quietly support different combinations of lookup, payment and authentication, but there was no standard way for an app to ask a host what it actually supports before sending a request. That left developers guessing URL schemes or assuming a service worked a certain way, with no safe place to park a transaction that is still being negotiated and isn't final yet. This fixes both: a host can advertise exactly what it offers, and a client can select one option and hold a work-in-progress proposal separately from real, spendable data.
Reference for an AI
Everything an assistant needs to answer questions about BRC-194 accurately, including what it depends on.
Summary
- Why
- Overlay hosts offered varying combinations of lookup, payment and authentication with no agreed way for a client to discover or select among them, and no safe place to put a transaction that isn't finished yet.
- What
- BRC-194 defines an HTTPS endpoint where an overlay host advertises signed, versioned capability profiles, and a companion envelope for storing non-final transaction or state proposals separately from finished, admitted data.
- How
- A client calls GET /overlay/v1/capabilities to fetch a signed manifest of services and profiles, picks exactly one profile ID matching its needs, then sends that profile ID and the manifest's digest on every subsequent request so the server can verify the selection before charging or disclosing anything, while…
What this lets you do
- Discover what an overlay host actually supports via one signed capabilities document
- Pick a specific profile ID so both sides agree on auth, payment and limits before any work happens
- Submit non-final or partially signed transaction proposals without pretending they are finished transactions
- Reject a request outright instead of silently downgrading to an unauthenticated or unpaid legacy path
- Keep a session running on its original agreed terms even if the host's capabilities change mid-flight
Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.
The specification
Abstract and status
BRC-101 describes useful facilitator capabilities but does not specify interoperable composite URL negotiation. This proposal defines concrete HTTPS capability discovery and explicit selection for incremental lookup, private data and non-final transaction proposals. Old [object Object]/SLAP URLs and /lookup and /submit keep their existing interpretation. A client does not gain these capabilities by guessing a URL scheme.
This proposed extension uses BRC-192 representations, digest/signature rules and errors. The profile identifiers in this packet are versioned contracts, not assertions that a deployed host implements them.
1. Discovery and binding
A host MAY expose GET /overlay/v1/capabilities, appended to its concrete advertised HTTPS base path. The response is a BRC-192 signed object of type capabilities, with this body:
type Capabilities = {
version: 1; identity: Identity; baseURL: string; chain: Chain;
issuedAt: U64; expiresAt: U64;
services: {
name: string; kind: 'lookup' | 'topic' | 'coordination';
rules: { id:string; parameters:Record<string,unknown> }; rulesDigest: Hex32;
profiles: {
id: string; authentication: 'none' | 'brc103';
payment: 'none' | 'brc105' | 'covenant';
maxRequestBytes: U32; maxResponseBytes: U32;
parameters: Record<string, unknown>
}[]
}[];
extensions?: Record<string, unknown>; critical?: string[]
}
The signature identity MUST equal identity. baseURL MUST equal the canonical selected base defined in section 6: HTTPS scheme, lowercase ASCII host, omitted default port, no user information/query/fragment, and a path with no trailing slash or dot segments. A root base has an empty path. Percent-encoded path components are preserved and MUST NOT be decoded into different route boundaries. A BRC-104 peer identity, when authenticated, MUST also match. SHIP/SLAP identity expectations and the caller's endpoint trust policy still apply. TLS control alone does not prove an advertised Bitcoin identity, and a valid identity signature does not authorize an untrusted endpoint.
An explicitly configured local-development caller MAY select an HTTP base for profiles that permit local HTTP, such as BRC-193. That exception is supplied by local configuration, never by an advertisement, redirect or manifest. Profiles requiring confidential HTTPS for private results retain that requirement. All identity, chain, path and selection checks still apply in local development.
Clients MUST reject duplicate (kind,name) services, duplicate profile IDs within a service, expired manifests, a different chain, or issuedAt > expiresAt. They apply a finite local freshness/clock-skew policy before accepting issuedAt; that policy cannot extend signed expiry. The complete manifest is limited to 256 KiB, 256 services and 32 profiles per service. A public document may omit private services; authenticated discovery may reveal a caller-specific manifest, which MUST use Cache-Control: private, no-store.
The capability digest is digest("capabilities", body). All packet endpoint requests other than discovery MUST send x-bsv-overlay-capability with that digest and x-bsv-overlay-profile with the exact selected profile IRI. Responses echo both. These x-bsv- headers are signed by BRC-104 when authentication is selected. The route/body identifies the service or an already scoped session. The server checks selection before charging, admitting or disclosing anything. A stale or incompatible selection returns context-changed; a missing required profile returns unsupported. Required capability failure MUST NOT silently fall back to a finite, unauthenticated or differently paid legacy request.
An established session may retain its opening manifest through its promised session lifetime even if discovery changes. The server records the selected contract and authorization scope. If it can no longer honor either, it returns a reset/error; it does not reinterpret a cursor using a new contract. New mutations and acquisitions require a manifest valid when initiated. Recovery operations can use the recorded contract until their promised deadline, even after manifest expiry.
2. Registered profiles
Profile IRIs are https://bsv.brc.dev/<category>/<number>#<fragment>:
| Profile | Authentication / payment | Required parameters |
|---|---|---|
overlays/0193#lookup-live-v1 | none or brc103 / none | replaySeconds: U64, sessionSeconds: U64, maxObservations: U32, maxWaitMs: U32 |
overlays/0194#proposal-v1 | brc103 / none | policies: {id:string,digest:Hex32,parameters:Record<string,unknown>}[], maxLifetimeSeconds: U64, retentionSeconds: U64 |
overlays/0195#private-publish-v1 | brc103 / none | maxPrivateBytes: U32, schemas: string[] |
overlays/0195#paid-lookup-v1 | brc103 / brc105 | recoverySeconds: U64, acceptancePolicy: ReleasePolicy |
overlays/0196#steak-potatoes-v1 | brc103 / covenant | recoverySeconds: U64, releasePolicies: ReleasePolicy[], domainProfiles: string[] |
overlays/0199#root-eviction-v1 | brc103 / none | maxTargets: U32, maxLifetimeSeconds: U64 |
Each profile's own bounds override larger advertised values. Byte, observation, lifetime and retention limits MUST be positive; wait limits may be zero. Unknown parameter fields are rejected for these version-1 profiles; future optional information belongs in manifest extensions. payment=covenant describes the purchase transaction, not an additional HTTP fee. A service offering a different payment arrangement needs a distinct explicit profile. Discovery, authentication and these parameter lists are not authority to invoke a wallet payment.
3. Non-final proposal model
Non-final data is an explicitly authorized domain proposal, separate from ordinary admitted outputs. It may contain a partially signed or currently non-final transaction, or an application state proposal with no transaction. This specification standardizes its envelope, concurrency and transport. It does not define a universal off-chain consensus or guarantee that the latest proposal can win an on-chain spend.
Each supported policy has an immutable identifier and digest and a locally installed validator. It MUST define participant authority, chain/anchor binding, allowed transaction incompleteness, transition meaning, expiry, conflict treatment and its eventual finalization relation. A digest names the exact policy version; remote code is never fetched and executed. Unsupported policies fail closed. Policy-specific assertions remain proposals even when every participant signs them.
type ProposalBody = {
version: 1; service: string; chain: Chain;
policy: { id: string; digest: Hex32 };
channel: Hex32; revision: U64; previous: Hex32 | null;
author: Identity; recipients: Identity[];
anchors: Outpoint[]; issuedAt: U64; expiresAt: U64;
operation: 'update' | 'withdraw';
payload: Bytes; transaction?: Bytes;
extensions?: Record<string, unknown>; critical?: string[]
}
type SignedProposal = { body: ProposalBody; signature: Bytes }
The BRC-77 signature type is proposal; signer equals author. transaction, when present, is raw Bitcoin transaction bytes, not trusted BEEF or a broadcast instruction. The policy parses it with strict size/work bounds and marks exactly which checks are incomplete. payload has the policy's canonical format and may contain other participant approvals. Recipients and anchors are duplicate-free, lexicographically sorted by identity and (network,genesisHash,txid,outputIndex) respectively. A profile can require private delivery, but cannot mistake signing for encryption.
The tuple (chain, service, policy, channel) identifies a proposal channel. channel is a random 32-byte identifier or a policy-defined commitment; its origin authority MUST be checked by the policy. Revision zero requires previous=null; every next revision is exactly previous revision plus one and names digest("proposal", previousBody). An update is accepted by compare-and-swap against the provider's current head. An identical digest is an idempotent retry; a different digest for the same predecessor is conflict. A disconnected provider can have a different authorized head: this local compare-and-swap rule does not create a distributed lock. Domain policy MUST resolve that difference before claiming a globally selected state.
4. Proposal endpoints and lifecycle
POST /overlay/v1/proposals/put accepts {version:1, proposal:SignedProposal}. The caller MUST be the author or a policy-authorized relay. A successful 200 returns {version:1, proposalId:Hex32, status:"recorded", expiresAt:U64} after durable storage and event-log commitment. It is neither STEAK nor a wallet receipt. Store proposals in a distinct namespace, with distinct UI lifecycle labels. No proposal may mark ordinary inputs spent, release purchased secrets, propagate through public GASP, or enter ordinary topic admission merely because it was recorded.
POST /overlay/v1/proposals/get accepts {version:1, service:string, policy:{id:string,digest:Hex32}, channel:Hex32} and returns {version:1, proposal:SignedProposal, state:ProposalState} as defined in section 6. Authorized recipients can subscribe through a BRC-193 service whose rules explicitly name the proposal partition; legacy queries MUST NOT gain provisional rows silently.
Withdrawal is a new signed revision authorized by the policy. A new update/withdrawal MUST be received before both its own expiry and the current head's expiry. Each expiry must be after issuedAt and at most issuedAt plus maxLifetimeSeconds; local clock policy also bounds future-issued objects. Expiry is exclusive and MUST invalidate active projections even without another network message. Full expired heads remain through expiry plus retentionSeconds; compact terminal-channel fences remain for the lifetime of the service identity, including across storage compaction and restart. Restarting a terminated channel requires a new channel identity. Revocation removes visibility according to current authorization but does not erase audit records contrary to retention policy.
POST /overlay/v1/proposals/finalize accepts {version:1, operationId:string, service:string, proposalId:Hex32, beef:Bytes, txid:Hex32}. Operation IDs use BRC-193 retry-ID constraints. The provider verifies caller authority, the policy's exact relation to the current active proposal, complete transaction evidence and its ordinary topic rules. First finalization of an expired/withdrawn proposal fails; replay of an already committed finalization may recover its original result through retention. This path requires explicit caller authorization for submission; it is never triggered by ingestion alone. The server durably links successful admission to the proposal and returns {version:1, proposalId:Hex32, state:ProposalState}. A conflict or uncertain admission remains unresolved until its exact operation is recovered. An admitted transaction is not thereby mined; a later conflict/reorganization invalidates applicable assessments and is exposed to subscribers. Finalization cannot silently replace a user's signed transaction with a newer one.
5. Implementation and compatibility
Use optional host and topic companion interfaces for capability description and proposal storage/validation. Existing topic managers, finite lookup plugins, STEAK validators and middleware constructors MUST continue working unchanged. A boolean on the legacy submit call is insufficient: separate storage, access checks, validation modes and lifecycle labels are required.
All errors use BRC-193's envelope/mapping. Enforce request/response bytes, finite proposal lifetime, per-principal channels, revisions, payload work and retention bounds. Network non-finality, an application's provisional status, processor acceptance and confirmation depth are separate concepts. Transaction nLockTime, sequence values or a seller signature alone do not establish revocable secret delivery or a trustless payment channel.
Conformance includes absent capability, wrong identity/origin/chain, expired manifest, signed-header tampering, no automatic downgrade or payment, unknown policy, concurrent revisions, identical retry, unauthorized relay, expired proposal, partial signatures, explicit finalization, public-feed isolation and changed chain view. An implementation MUST demonstrate an actual domain policy; the generic envelope alone does not qualify a product as supporting arbitrary non-final protocols.
6. Selection registry and complete proposal lifecycle
The profile/service-kind matrix is closed: lookup-live and paid-lookup require kind lookup; private-publish, steak-potatoes and proposal require kind topic; root-eviction requires kind coordination and name root-advertisements. A request's service/topic selects exactly one entry of that kind. Session/acquisition/publication/channel records retain that entry; subsequent identifier-only routes resolve it before interpreting a body. A profile header alone cannot select an arbitrary enabled service. Profile parameter arrays have 1–32 unique entries. ReleasePolicies contains complete BRC-196 tagged objects compared by JCS equality, not descriptive names; mined confirmations must be positive. Domains and schemas are immutable supported IRIs. Acquisition recoverySeconds is at least 86400 and is the minimum promised interval after payableUntil/purchaseUntil, not a maximum session lifetime. A longer Offer promise takes precedence. Parameter combinations contrary to kind/authentication/payment/profile bounds invalidate the manifest before wallet work.
rulesDigest = digest("service-rules", rules). Rules.id is an installed immutable contract IRI whose registration defines the exact parameter schema, selection/order/visibility semantics and compatibility. Its parameters are public and included in the manifest. A descriptive string or source-code hash with unspecified build inputs cannot stand in for this contract. Proposal policy digests similarly equal digest("proposal-policy", {id,parameters}); their parameters appear in the capability. The wire proposal retains id/digest and resolves parameters against that selected, retained manifest. An unsupported registration is unsupported, not code downloaded from its IRI. Versioned registrations and pinned dependencies are listed in the packet registry. A changed interpretation requires a new IRI and selection, not mutation of an existing version.
Canonical bases are ASCII URI strings, at most 2048 bytes. A DNS name uses lowercase validated A-labels; Unicode host input must first be converted by the caller's IDNA policy and the resulting ASCII origin approved explicitly. Uppercase host input normalizes to lowercase. Trailing DNS dots, empty labels, backslashes, zone identifiers and percent-encoded hostnames are rejected. IPv6 literals use RFC 5952 lowercase compressed form in brackets. A nondefault numeric port is retained without leading zeros; 443 is omitted for HTTPS (80 for explicitly allowed local HTTP). Path percent escapes use uppercase hex. Reject escapes for slash, backslash, dot, percent, NUL or unreserved ASCII; reject raw/encoded dot segments and empty intermediate path segments. This deliberately narrows URI spelling to avoid router-dependent equivalences. Raw non-ASCII path input is UTF-8 percent-encoded before approval; no decoded route boundary changes. A terminal slash is removed; root slash becomes empty. Append the literal endpoint suffix to this canonical base, never resolve it as an origin-root relative URL. For example https://EXAMPLE.test:443/api/ becomes https://example.test/api, whose open URL is https://example.test/api/overlay/v1/lookup/open. https://example.test:8443/a%20b is preserved. https://example.test./, /a/../b, /a%2Fb and /%2e/ are invalid. https://[2001:db8::1]:8443/api is valid; https://xn--bcher-kva.example/api is an ASCII base, not authority to trust that host.
Discovery bootstraps through the selected TLS origin and expected identity policy. An unauthenticated public discovery still verifies the manifest BRC-77 signature. BRC-103 challenge exchange may establish identity before a caller-specific GET; no paid middleware is used for discovery or negotiation errors. Authenticated application responses, including errors after negotiation, echo and authenticate the exact request selection headers. A failure before authentication/selection can return an unauthenticated transport error; it cannot be interpreted as a trusted protocol state, retried with payment, or repaired by silent downgrade.
Clients persist the complete signed manifest and selected service/profile with every operation before any effect. For recovery the server first authenticates the caller and resolves the retained record/selection, then evaluates its recorded deadlines; it does not reject that operation solely because today's manifest differs or the old one expired. A current capability may recover an old record only if it names the identical service/profile/rules and the record explicitly authorizes that selector; otherwise use the retained digest. Key/endpoint rotation must retain the original authenticated recovery endpoint/identity through obligations, or supply a separately verified migration accepted by the buyer. A redirect or new key alone cannot transfer authority. Missing retained state is an operational failure, not permission to create a replacement acquisition.
type ProposalState =
| { status:'active'|'withdrawn'|'expired'; recordedAt:U64 }
| { status:'finalizing'; recordedAt:U64; operationId:string; txid:Hex32 }
| { status:'finalization-failed'; recordedAt:U64; operationId:string;
txid:Hex32; reason:string; globalOutcome:'unknown' }
| { status:'finalized'; recordedAt:U64; operationId:string;
txid:Hex32; steak:STEAK; assessmentContextId:string }
Before finalization reservation, validate current authorization, active head, request/proposal relation and strict transaction shape. A malformed/unauthorized attempt does not consume an operation ID. After these checks, atomically CAS active head to finalizing and store (caller,service,operationId), proposalId, txid, exact raw transaction and pending admission job. Semantic identity excludes only validated alternate BEEF proofs of that same raw transaction. Reusing the ID for another proposal/txid conflicts; another operation for an already bound proposal resolves that original state and cannot start a second transaction. Persist reservation before external admission. Updates, withdrawal, expiry and this reservation serialize on the same channel: if termination wins, finalize fails; if finalization wins, subsequent updates/withdrawals conflict and expiry does not cancel the committed work.
A definitive local admission rejection records finalization-failed and its reason; uncertainty remains finalizing and is reconciled by exact txid and the durable admission operation. An empty duplicate STEAK is not a negative outcome. Admission committed before channel linkage is recovered from retained topic processing, then linked without double effects. Finalization-failed, finalized, withdrawn and expired terminate the channel; no later author revision reopens it. Finalized is historical and survives a reorganization, with separate assessment-invalidated events/currentness. It never claims permanent mining success. Failed local admission also says nothing globally definitive about future mining.
Current read authorization is the intersection of the installed policy's author/recipient permissions and the host's current access policy, evaluated on get, retry and serialization. Missing and unauthorized get use the same not-found response. Compact terminal fences retain (chain,service,policy,channel) and terminal status for the service identity lifetime; loss of those fences requires retiring that service identity/namespace, not silently accepting revision zero again. Every lifecycle transition, including finalizing/failed/finalized and timer expiry, emits proposal-state atomically with storage. It names the original author-signed head without changing its bytes. An explicit recipient visibility removal uses proposal-remove only while authorized to disclose that identifier; otherwise terminate/reset the private source. Anchorless proposals therefore need no fictitious outpoint to expire or disappear.
The concrete policy https://bsv.brc.dev/overlays/0194#author-document-v1 has parameters {maxTextBytes:U32} (1–4096). It allows an authorized author to maintain an opaque document shared with 1–32 sorted recipients including the author. Payload is canonical UTF-8 JCS {text:string}, with no other fields; empty text is allowed. Each revision retains the same author/recipients/anchors; there are zero or one anchors, bound to the selected chain. Transaction is forbidden in this policy's proposal envelope. Only the author may put/finalize; relaying is unsupported. Any currently authorized recipient may read. Withdrawal is an author-signed next revision and is terminal. An anchorless channel may finalize to a valid fully verified transaction with output zero containing exactly a one-satoshi OP_FALSE OP_RETURN minimal push of ASCII PRP1 followed by the 32-byte proposalId. If an anchor exists, input zero must spend it as well. Finalization requires the author's authenticated explicit request and successful configured topic admission; the document signature itself is not a Bitcoin spending signature. This policy does not transfer title or release a secret. It provides a complete non-final off-chain document example without claiming support for arbitrary partially signed transactions.
A BRC-194 proposal is an authenticated application intent, not a verified Bitcoin spend. BRC-192 section 10 can select a non-final transaction only when its actual signed bytes, dependencies and Script checks qualify under the explicitly enabled non-final policy. A proposal alone cannot supply that eligibility, consume an input, trigger finalization or authorize a wallet action. Neither proposal revisions nor provider sequence numbers replace the componentwise Bitcoin sequence rule. BRC-197 remains final-sequence/zero-locktime only.