← Architecture map

Woodpecker v2 decisions

Decisions

D1

Tenant = wallet: one Durable Object per wallet; the sign-in is a wallet signature that the DO verifies (D47).

Tenant = wallet. One Durable Object per wallet (name chainId:address), one Harness, the pi-durable tables as today. A conversation is (wallet, conversationId).
Sign-in: the wallet signs a challenge (domain, nonce, expiry); the Worker issues a short-lived credential bound to the address; the DO accepts only its own address.

D47 · D1's note "the Worker issues a short-lived credential bound to the address" → D47 (the DO verifies the sign-in; the Worker only forwards).

D2

A conversation's agent is derived from a per-wallet root the runner keeps in its DO; the agent holds no balance and pays no gas.

A conversation's identity is derived from a per-wallet root that the runner generates and keeps in its DO storage. Formulas unchanged: keccak(root, "conversation", n), subagent and schedule slots as today.
The identity has no balance and pays no gas. Contract A, gateways, directory and Portal statistics see the same client and identity shapes as today.

D3

Service kinds in the descriptor: runner, store (now memory), model, tool; one gateway may declare several kinds.

Service kinds in the descriptor: runner (hosts conversations, derives identities, speaks to the Console), store (durable conversation storage; reusable by other services), model (chat API usable as the agent's model), tool. One gateway may have several kinds.
The Services page shows the kinds as badges.

D51a, D52a · D3 and D34 kind lists → D51a and D52a: the kinds are runner, memory, model, tool, host.

D3a · D3 and D34 kind lists → D3a and D34a: the kinds are runner, memory, model, tool, host, store.

D3a

The kinds are runner, memory, model, tool, host and **store** (D74): a store keeps an extension's large data, paid per MiB started, reads free for the owner; it is chosen at install, with no default. One gateway may declare several kinds, as steven@68df/datavault declares store and tool.
2026-11-03, with D74.

D4

The runner is a service the wallet chooses; the New conversation dialog requires one.

The runner is a service chosen by the wallet. The New conversation dialog starts with a required Runner select; without a runner in My services it refuses to create and links to Services.
Resolves "who pays for the Worker + DO": the runner's operator is paid in the runner's token.

D5

A descriptor may declare uses with bundled or client payment (deferred by D48).

A descriptor may declare uses: the services it depends on, each with payment: "bundled" (in my price, invisible to the client) or payment: "client" (the client pays it; the runner signs with the conversation identity).
Recommended default (not yet decided): metered dependencies client, the provider's own infrastructure bundled.

D6

Two token kinds: direct (one gateway's unit) and proxy (a basket of child tokens); proxy tokens are deferred (D48).

Two kinds of service token. Direct: one gateway's unit, as today. Proxy: a composite service's unit backed by a basket of child tokens, with covers(child), rate(child) (proxy units charged per 1 child unit; rate 2 means 1 tDurable costs 2 tAcmeRun) and exchange(child, amount, to).
A direct token implements the three functions as "covers nothing". Rate definition fixed 2026-10-22 after the mock found D6 and D7 inverted; D7's arithmetic is the reference.

D7

The Intent gets an explicit fund field for nested billing (deferred by D48).

The Intent gets an explicit fund field: the token the client authorized. Direct billing: fund == token. Nested billing: token = the child's token, fund = the proxy. New typehash; every gateway and the runner upgrade together.
Contract A: reserve checks the authorization of (client, identity, fund) and fund.covers(token), reserving max × rate; settle pulls actual × rate of the proxy from the client to the proxy's owner and calls exchange(token, actual, payTo) from the basket. An empty basket fails the settle; the gateway releases and does not answer.

D8

One hop only: the fund must cover the token directly.

One hop only: fund must cover token directly. A proxy of a proxy is not supported.
Depth-2 nesting (a dependency spending further on the client's behalf) would need sub-authorization on A; not in scope.

D9

The proxy owner buys child tokens and deposits them into the basket (deferred by D48).

The basket: the proxy's owner buys child tokens from each child's owner and deposits them into the proxy contract. No minting rights are granted to proxies.
Portal shows basket levels; the owner refills.

D10

wpCredit is an independent credit token, not a proxy and not a fund; ≈ wp is an exchange-rate view of caps, balances and spends.

wpCredit is not a proxy and not a fund. It is an independent credit token that exchanges with every ServiceToken (each token publishes its rate).
The ≈ wp figures in the mock are this exchange-rate view of caps, balances and spends. Purchase and exchange flows: open.

D54, D57 · D10's note "Purchase and exchange flows: open" → D54–D57 (issued by Woodpecker; credits to the token's owner; paths A and B; changeable rate). Still open: how a wallet buys wpCredit from Woodpecker; the starter pack's wpCredit amount.

D11

The agent's model is a gateway with an agent-chat route; the runner holds no provider credentials.

The agent's model is a gateway too: a chat gateway exposes an agent-chat route (tools, tool_choice, streaming, tool messages pass through; metered by usage). The runner registers such gateways as models. No provider credentials in the runner.
Precedent: today's llm-gateway provider in the Engine (gpt-luna's lane, tLLM). The round-2 template gateways need the route.

D12

The first mock UI: three menu items, authorizations grouped per identity, caps entered inline.

Mock UI (done): three menu items (Wallet & Settings, Services, Conversations); Authorizations grouped per conversation identity with ≈ wp; caps entered inline next to the Model select and on each checked tool, one value per token, total in wp and the signature count; conversation view with the Identity card on top and one card with the tabs Authorization / Spends.
Commits ffec0aa, b375fc5, 4149420; live at https://woodpecker-console-mock.pages.dev

D60 · D12 / D33 "the total ≈ wp in the footer" → D60 (the total is the editable wpCredit cap in the budget block).

D12a · D12 “three menu items” → D12a and D69 (Extensions, and one item per installed extension).

D12a

Four menu items (Conversations, Services, Extensions, Settings) plus one per installed and enabled extension, in the person's order (D69). The rest of D12 stays superseded as noted.
2026-11-03, with D69.

D13

Order of work: record the decisions, finish the mock, write the v2 documents, then rewrite the modules.

Order: record now (this file); finish the mock; write the v2 architecture documents in this repository; then rewrite every module. The current system stays as it is until then.

Decisions of 2026-10-22 (the Deploy flow in the Console)

D14

The Console deploys services itself; the Console wallet is the token owner; the v1 Portal is not merged.

The Console deploys services itself: a "Deploy a service" entry on the Services page replaces the /devportal skill and the Portal's deploy pages. No Claude Code. The Console wallet is the token owner and the paying client; there is no separate Portal wallet.
The Portal is not merged into the Console; the deploy flow is mocked on its own.

D15

Deploy prerequisites: a runner, a model and a host service (kind host, D52).

Prerequisites of a deployment: My services must hold a runner, a model and a cloud service (kind cloud: a wrapped Cloudflare account that deploys Workers, writes secrets, binds DOs, reads logs; priced per operation). Missing ones are listed with an Add link.
First cloud service: Steven's own account. Other clouds later, same kind.

D52a · D15 "kind cloud" → D52a (host); the service woodpecker@0a1b/cf keeps its name and tCloud.

D16

A deployment runs as an agent conversation on the runner, with its own agent, caps and spends.

The deployment runs as an agent conversation on the runner (model + tools), not as a fixed task. The agent asks what the skill asked (preset, service id, symbol, models, limits, mapping), calls the cloud service, deploys the token with the wallet's signatures, binds, verifies. The deployment is a conversation like any other: identity, caps, spends.
The transcript shows every tool call with its price and every signature.

D17

The upstream API key goes from the browser straight to the host service's secret store; the agent sees only its fingerprint.

The upstream API key is entered in a dedicated field above the agent conversation, outside the chat. It goes from the browser straight to the cloud service's secret store. It never enters the conversation; the agent sees only its fingerprint.
Rule carried over from the skill (never print, never pass a value).

D18

Directory rows have an Add button; a host service is listed like any other service.

Directory rows get an explicit Add (to My services) button; a cloud service appears in the directory like any service.

D19

Verify is a real paid call from the Console wallet to its own new gateway.

Verify stays a real paid call, from the Console wallet to its own new gateway.

D20

Direct services first; the bundled type is reasoned again later.

First the direct (non-bundled) service type. The bundled (proxy) type and its templates are reasoned again later.

Decisions of 2026-10-22 (bundled services, languages, architecture map)

D21

A bundled service has a proxy token and declared dependencies (deferred by D48).

A bundled service is a service with a proxy token and declared dependencies. Its first template is the runner (a Worker + a Durable Object class). A runner has no upstream API key; it depends on a store (payment: client, paid through the proxy) and on the owner's cloud compute (bundled).
Other bundled templates (composite gateways) later.

D22

Deploying a bundled service adds the proxy token and the funded basket (deferred by D48).

Deploying a bundled service adds to the direct flow: choose the template, pick the dependencies from the directory, set the proxy token's symbol and the rate per covered child token, deploy the proxy token (covers, rates), fund the basket (buy child tokens from each child's owner, paid in wpCredit at the child's published rate, then deposit into the proxy), register with uses.
D9 and D10 together: the purchase is a wpCredit exchange; no minting rights.

D23

Verify of a runner is a real one-turn conversation on it (deferred by D48).

Verify of a runner is a real one-turn conversation on the new runner: a turn paid in the proxy token plus one store commit settled through the exchange path (fund = proxy, token = the store's token, basket decreases).
The nine lines of Verify are adapted per kind.

D24

A proxy service shows its basket levels with a Refill action (deferred by D48).

A proxy service deployed by this wallet shows its basket levels (per child token) with a Refill action in My services.
Refill repeats the purchase and deposit.

D25

The Console and the architecture pages are trilingual: English, Chinese, Spanish; identifiers never translate.

The Console and the architecture map are trilingual: English (default), Chinese, Spanish, switch in the top bar, choice kept per browser. Identifiers never translate: service names, tool names, token symbols, addresses, hashes.

D26

The architecture map lives in this repository as one HTML page, published on its own Pages project.

An architecture map of v2 lives in this repository (docs/architecture/) as one HTML page in the format of https://woodpecker-runtime-map.pages.dev/ (boxes and edges, flow buttons, info panel, legend), published at a Pages project of its own.
It is the reference for the v2 architecture documents.

Decisions of 2026-10-22, later (Steven's review of the deploy flow)

D27

The bundled deployment is dropped from the mock.

The bundled deployment is dropped from the mock (too complex for now). Proxy tokens, runners and stores stay in the directory and the decisions (D6–D9, D21–D24) as decided-not-mocked.
The deploy page offers direct services only.

D28

A deployment runs to the end on its own; it stops only when it needs the person.

A deployment runs to the end on its own. No step-by-step Next. The agent stops only when it needs the person: a question (answered in the chat) or a wallet signature (a Sign button on the card). Errors are reported by the agent in the conversation.

D29

A service deployed by this wallet opens a management page: token, submitter, Distribute, Statistics, Verify.

My services marks the services deployed by this wallet and they open a management page: what the Developer Portal shows for a service (state, origin, token, submitter and its gas, Verify, Statistics per payer and per call, Distributions) plus Mint to an address (Distribute) and an address book. Other people's services have no management page.
Replaces the Portal's Services, Tokens, Distribute and Statistics pages for the wallet's own services; the Portal itself is not merged (D14).

Decision of 2026-10-23 (Steven's review of the self-running deploy)

D30

A deployment asks nothing in the chat; it pauses only for wallet signatures: the caps, the contracts card, the verify card.

A deployment asks the person nothing in the chat. Everything it needs is given before it starts: the prerequisites (runner, model, cloud), the upstream API key, and the service details (preset, service id, token symbol, label) in a small form with prefilled suggestions. From Deploy to done the agent runs on its own. It pauses only twice, for wallet signatures: once for the contracts (fund submitter, deploy token, setService, register in the directory, one card, one Sign that signs them in sequence) and once for verify (mint, approve, authorize, revoke, one card). The progress rail stays and must reflect each step live.
Corrects D28's "a question answered in the chat": there are no questions. The chat input remains for the person to interject, never as a required step.

D56b · D30 "four signatures" for the Contracts card → five: setMintAllowance(A) joins fund submitter, deploy token, setService and register (D56b).

Decision of 2026-10-23, later (wallet placement and signing)

D31

Wallet and Settings are separate; the wallet is a permanent block at the bottom of the sidebar.

Wallet and Settings are separate. The menu has Settings, Services, Conversations. The wallet lives at the bottom of the sidebar as a permanent block (address, network, total ≈ wp); it opens the wallet panel (balances, grouped authorizations with Revoke, export / import key, sign out). The top-bar address and Sign out go away.

D32

Every signature goes through the wallet's signing panel; one Sign signs the whole group in sequence.

No signature card inside a conversation or a page. Every wallet signature (sign in, new-conversation approve / authorize, the deployment's caps, the Contracts group, the Verify group, fund submitter, mint, revoke) is requested through the wallet: the wallet slides out from its sidebar block with a signing panel that lists the transactions of the request; one Sign signs the whole group in sequence (each line turns signed with its tx), Reject fails that step and the agent reports it. The page or conversation only shows "waiting for the wallet" until then.
The agent's run pauses only at those requests.

D64 · D32 "the popover … or clicks Revoke" → D64 (the Console stops the DO from signing; on chain authorize with cap = used; the Console change comes with the Console's D64 alignment, not yet briefed).

D33

No Title field in the New conversation dialog: the title is the first message; the dialog's one big figure is the total ≈ wp.

New conversation dialog: no Title field. A conversation is created without a title; its title is the first 80 characters of its first message (whitespace collapsed), as the Engine's defaultTitle and the old Woodpecker do; it can be renamed later from the conversation header. The dialog's summary is the total authorization in wp, shown as the one prominent figure of the dialog (the amount the person remembers), next to the Create button; per-token caps stay small and inline. Signing after Create goes through the wallet panel (D32), never a button in the dialog.
2026-10-23

D60 · D12 / D33 "the total ≈ wp in the footer" → D60 (the total is the editable wpCredit cap in the budget block).

Decisions of 2026-10-23 (kinds and pricing, from the discussion with Steven)

D34

A kind is a contract of interfaces (runner, memory, model, tool, host); a label is only a display name.

kind is a convention, not a label. A service that declares a kind promises the interfaces of that kind, and the Console relies on them without reading anything else. A service may declare several kinds. label is only the developer's display name in the descriptor.
Required interfaces per kind: model — an agent-chat route (OpenAI-style chat with tools, tool messages and streaming, metered by usage) plus /pricing; tool — one or more tool routes with a JSON-Schema parameter block and a pricing rule each, plus /pricing; runner — create / submit / watch / abort a conversation, derive identities, pay dependencies with the conversation identity; store — the durable conversation storage API (commit, read, scan); cloud — deploy a Worker, put a secret, bind a Durable Object, read logs, delete. A kind comes from the template or preset the service was deployed with; it is never typed by hand. Whether the directory contract records kinds is the open item above.

D51a, D52a · D3 and D34 kind lists → D51a and D52a: the kinds are runner, memory, model, tool, host.

D34a · D3 and D34 kind lists → D3a and D34a: the kinds are runner, memory, model, tool, host, store.

D34a

Kind **store** joins the list of D34: its interfaces are put, list and delete per owner (the store methods of the extension API, D76), priced per MiB started, reads free for the owner. Kinds: runner, memory, model, tool, host, store.
2026-11-03, with D74.

D35

Pricing is one of fixed, tiered or metered, then numbers; the contract only ever sees (max, actual).

Pricing is a choice among fixed forms, then numbers. Each tool route (or the model route) carries one pricing rule: fixed (one price per call), tiered (a measure of the request — a number at a path, a string length, code points or body bytes — and [limit, price] tiers), or metered (a max rule for the quote and an actual rule from the response, or a shipped preset such as openai-usage, apify-usage, bytes-tiers), plus maxPerCall. The developer picks the form in the deploy form and sets the numbers; the preset proposes both. The contract only ever sees (max, actual); the Console shows the rule as fixed · 1 tR2 per call, tiered · ≤ 6 tVoice, metered · ≤ 100 tGpt, with the ≈ wp view.
Same three forms as the v1 gateway spec §3.1; period and conditional stay reserved. In the mock's deploy form the pricing block is implied by the preset; a later change may expose form + numbers as fields.

D36

Each kind and preset has a default pricing; the preset other is discussed with the agent and confirmed on a card.

Pricing comes with the kind and the preset; other is discussed with the agent. Every kind has a default pricing form (model: metered by usage; tool: fixed per call, or tiered when the preset defines a measure; runner: metered per turn; store: fixed per commit; cloud: metered per operation), and every known preset carries its numbers. The deploy form shows the resulting rule in one editable line. The Preset select gets an other entry for an API no preset knows: then, and only then, the agent asks in the chat (what the API is, its routes and parameters, a free check path, the pricing form and numbers), proposes a summary card, and continues only after the person confirms that card.
The one exception to D30 (no questions in the chat). The run after the confirmation is the same as for a preset.

D37

label is a free search tag, never part of the service name.

label is a user-defined tag for search. It is free text the developer chooses, shown as a small tag next to the display name and matched by the search box; it is not an identifier and never part of the name.
Several tags may come later; one text field for now.

Decisions of 2026-10-23 (the conversation view)

D38

The conversation view: a spends strip above the composer; the authorization is a button with meters and a popover with editable caps and Revoke.

The conversation view is redesigned around the composer. The transcript takes the width. Above the composer runs a spends strip: one scrolling line of chips, newest first, one chip per paid call (kind icon, name, actual amount, state), with a link to the full spends sheet. The authorization is a button in the composer bar: collapsed, it shows each token's remaining as a small graphic meter (ring or bar, remaining / cap) and the total remaining ≈ wp; opened, it is a popover above the composer with the identity (address, copy), the per-token rows (cap, used, reserved, remaining, ≈ wp), editable caps with a Submit changes action that asks the wallet for a new authorize (through the wallet panel, D32), and Revoke per token. No separate Identity / Authorization / Spends cards on the right.
The functions stay as they are; only the layout and the interaction change. The look follows a written design plan (tokens, type scale, layout concept) made with the design skill.

D39

Every service has a simple monochrome icon by kind; a developer may pick one at deploy.

Every service has an icon. Default by kind (runner, store, model, tool, cloud); a developer may pick one at deploy from a small set of simple monochrome vector icons (lucide line icons); no emoji, no colored or pictorial icons (amended 2026-10-23). It shows wherever the service is named: Directory and My services rows, the New conversation dialog, the conversation header, the spends chips, the management page.

D40

The wallet opens in two steps: a compact popover, then the full dialog with the tabs Balances and Authorizations.

The wallet opens in two steps. The sidebar block's expand control opens a compact popover first (anchored above the block, about the block's width ×1.3): address with copy, network, ETH, total ≈ wp, the count of active authorizations, the top token balances, and two actions: Open wallet and Sign out. Open wallet opens the full wallet dialog, which has two tabs: Balances and Authorizations (the footer keeps Export key, Import key, Sign out).
2026-10-23, Steven's review of Change 10.

D41

A model is not a tool: a chat gateway is offered only as the conversation's model.

A model is not a tool. A chat gateway has kind model and is offered only as the conversation's model. Its chat route is not listed among the tools. (Calling one model from another is not a use case of v2.)
2026-10-23, Steven's review of the dialog; in the Change 15 draft.

Decisions of 2026-10-23, later (provider, starter tokens, wording, effort)

D42

Own services live only in My services, with Manage; the Directory lists other owners' services, with Add.

Own services live only in My services. A service this wallet deployed never appears in the Directory; it is in My services from deployment on, with Manage. The Directory lists other owners' services; Add copies one into My services.

D43

The provider woodpecker@0a1b publishes the runner, the llm, the memory and the host service (cf); a new wallet starts with them.

The provider woodpecker@0a1b publishes a runner (Worker + DO bundled in one service, no store dependency, metered per turn), an llm model and a cf cloud service. A new wallet starts with the runner and the llm in My services; it adds the cloud service before its first deployment.
Solves the chicken-and-egg of the first deployment without a bundled-deployment flow.

D43 "no store dependency" still holds: the runner commits to the memory service the wallet chose, paid by the conversation's agent, not as a runner dependency.

D63, D64 · D43 "metered per turn" on the map → D63 + D64 (one round = 1 tRunner; receipts; batch settle).

D44

Starter tokens for a new wallet: 50 tRunner, 50 tLLM, 50 tMemory and 20 tCloud.

Starter tokens: the provider gives a new wallet 50 tRunner, 50 tLLM and 20 tCloud (and the wpCredit balance), enough to try conversations and deploy a first service.
Shown in the wallet as "Starter pack from woodpecker@0a1b".

D45

Signing requests speak plainly: the purpose and the wp amount first; the conversation identity is called the agent.

Signing requests speak plainly. The panel leads with the purpose and the wp amount; the transactions sit under a collapsed Details; the conversation identity is called the agent (e.g. "the agent of ‹title›"); the authorization contract is "Woodpecker's authorization contract"; one network-fee line.
Change 17.

D46

Effort is a conversation setting (off … max): a default in Settings, changed from the conversation header, no signature.

Effort (thinking level) is a conversation setting: off, minimal, low, medium, high, xhigh, max; chosen in the New conversation dialog next to the model, changeable from the conversation header, with a default in Settings. No signature: it is a runner parameter, not a payment.
Change 18.

D53 · D46 "chosen in the New conversation dialog" → D53 (the dialog has a Memory select instead; effort starts from the Settings default and changes from the conversation header).

Decisions of 2026-10-23, evening (sign-in, the map's scope and form)

D47

The runner's Worker only forwards by wallet address; the DO verifies the sign-in itself: challenge, signature, session credential.

The runner's Worker only forwards; the DO verifies the sign-in itself. The Worker reads the claimed wallet name chainId:address (lower-case) from the request, checks only its format, takes the Durable Object by that name (getByName) and forwards the request. The DO issues the challenge (domain, nonce, expiry; the nonce kept in memory), verifies that the signature recovers to its own address (ctx.id.name), stores a session credential with an expiry in its own storage and checks it on every request; sign-out deletes it. No global secret in the Worker, no database, no registry of DOs; a junk request writes nothing.
Replaces the D1 note "the Worker issues a short-lived credential". Payment is not a sign-in concern: any wallet may sign in; the runner checks the agent's tRunner remaining on contract A before every turn. Alternative recorded, not chosen: the embedded wallet key signs every request (no challenge, no session); it would prompt on every request once external wallets are supported.

D47a · D47 “the signature recovers to its own address” → D47a (or to an identity contract A shows authorized by the wallet, D73).

D47a

A service accepts a sign-in by the wallet's signature, or by an identity that contract A shows authorized by the wallet: the challenge is the same; the signature recovers to an identity, and the service accepts it when tokensOf(wallet, identity) on contract A is not empty (D73). A brand-new wallet signs in with the wallet itself the first time. The Worker still only forwards; the DO still verifies.
2026-11-03, with D73.

D48

Current scope is direct tokens only: proxy tokens, nested billing, the Intent fund field and the bundled deployment are deferred.

Current scope: direct tokens only. Proxy tokens, the store service, nested billing, the Intent fund field and the bundled deployment (D6–D9, D20–D24, D27) are deferred. They stay in this log; the architecture map and the first v2 documents do not show them. The runner bundles its Worker and DO in one direct service (D43); the directory's uses field is deferred with them.
Steven, 2026-10-23.

D51, D51a · D48 "the store service is deferred" → D51 and D51a (the memory service, a direct service, is in scope); proxy tokens, nested billing, the Intent fund field and the bundled deployment stay deferred.

D56a · D48 "the Intent fund field is deferred" → D56a (needed by path B; proxy tokens, nested billing and the bundled deployment stay deferred).

D49

The architecture map shows numbered steps with a narration panel; response arrows are allowed; the runner is one frame around its DO.

The architecture map shows numbered steps. Each flow is an ordered list of steps. The step number sits on the edge; the info panel lists the steps as sentences when a flow is selected. A response arrow is allowed when it carries a step (thin, open arrowhead; legend entry "response"). The Worker router is not a box: the runner is one frame woodpecker@0a1b/runner that contains the DO, with a note that the Worker is the entry and forwards by wallet address.
Change 21.

D49a

A flow step that happens inside one party (for example "import the key" in the Console) has no edge; its number badge sits in the top-right corner of that party's box, and the box lights up. Everything else of D49 stands.
Change 35, WoodpeckerMock5.

Decisions of 2026-10-23, late (where state lives; the store returns; the hosting kind)

D50

Where state lives: the chain (balances, authorizations, directory, own services), the memory service (conversations, settings record), the runner's DO (identity root, sessions, running turns), the browser (key, language, theme, last-used runner and memory).

Where each piece of state lives. The wallet is a key and holds nothing. On chain: token balances; authorizations (cap, used, reserved per wallet, agent, token); the service directory; which services are the wallet's own (directory owner). In the wallet's store (D51): the conversations (transcript, title, status, spends) and the wallet's settings record (My services and their default caps; default model, effort, runner and store). In the runner's DO: the identity root, the sessions, the running turns and a cache of the conversations it works on. In the browser only: the wallet key (exported and imported by the person), language, theme, and the last-used runner and store, with the provider's as the defaults (D43).
Steven, 2026-10-23: "add it; it does not work without this". The Conversations list and My services are read from the store; the Directory and the balances from the chain; a new browser finds the settings record in the default store and the preferred runner in it.

D50a · D50's lists → D50a (the identity root, the install records, the configuration namespaces, the store service, caps for every identity; D74).

D50a

The browser keeps the identity root (and the wallet key until the wallet is external), the language, the theme, the menu order, and the last-used runner and memory; the memory service keeps the conversations, the settings record with the install records, and the extensions' configuration namespaces; a store service keeps an extension's large data; the chain: caps for every identity (the agents, the Console's, the extensions', the devices') (D74).
2026-11-03, with D74.

D51

The memory service: a direct service (woodpecker@0a1b/memory, tMemory, fixed 1 per commit) that keeps the conversations and the settings record; the runner commits each completed turn; the owner signs in to it as to the runner.

The store service returns, as a direct service. Kind store (D34): durable conversation storage with commit, read and scan, plus the wallet's settings record. Default woodpecker@0a1b/store, token tStore, fixed 1 tStore per commit (D36), in My services from the start with the runner and the llm (D43), 50 tStore in the starter pack (D44). The runner commits each completed turn to the conversation's store with an Intent signed by the conversation's agent (token = tStore); the store's submitter reserves and settles on contract A. The owner wallet signs in to the store as to the runner (D47) and reads its conversations and writes its settings record free of charge; the agent that committed a conversation may read it. The store is chosen per wallet in Settings and per conversation in the New conversation dialog, next to the runner. Any wallet may deploy its own store from the store preset (a Worker + Durable Object storage, no upstream key).
Revises D48 for the store only: proxy tokens, nested billing, the Intent fund field and the bundled deployment stay deferred. Consequence: the runner is replaceable; the conversations stay in the store when the runner changes.

D51b · D51 “the conversations and the settings record” → D51b (and the extensions' configuration namespaces; D74).

D51a

The service of D51 is called memory: kind memory, default woodpecker@0a1b/memory, token tMemory, fixed 1 tMemory per commit. Nothing else is renamed: the "settings record" keeps its name, conversations keep theirs.
Steven, 2026-10-23: "memory, then: change store to memory and do not change the other names".

D51b

The memory service also keeps the configuration namespaces of extensions (ext:<id>), bound to their identities at the first write; 1 tMemory per commit, within the manifest's quota (D74). The settings record gains the install records (D72).
2026-11-03, with D74.

D52

The kind cloud is renamed host; woodpecker@0a1b/cf keeps its name and its token tCloud.

The kind cloud is renamed hosting. A hosting service deploys and runs the Workers of gateways, runners and stores: deploy a Worker, put a secret, bind a Durable Object, read logs, delete (the D34 interfaces, unchanged). The service woodpecker@0a1b/cf keeps its name and its token tCloud; only its kind changes. Kinds are now runner, store, model, tool, hosting.
Proposed word hosting (alternatives: host, deployer); Steven to confirm the word.

D52a

The kind of D52 is host (not hosting). woodpecker@0a1b/cf and tCloud unchanged. Kinds: runner, memory, model, tool, host.
Steven, 2026-10-23.

Amendments of 2026-10-23, late (names fixed by Steven)

D53

The New conversation dialog has a Memory select in place of Effort; effort comes from the Settings default and is changed from the conversation header.

New conversation dialog: the Effort select is removed; in its place a Memory section with the select of the conversation's memory service (default woodpecker@0a1b/memory, chosen like the runner) and its cap row (tMemory). Effort is no longer chosen in the dialog: a conversation starts with the default effort from Settings and can be changed from the conversation header (D46 amended). The Settings page keeps Default runner, Default model, Default effort and Theme, and gets a Default memory select.
Steven, 2026-10-23: "it is the New conversation page" (corrects the first reading, which put the change on the Settings page).

Decisions of 2026-10-24 (wpCredit, from the discussion with Steven)

D54

wpCredit is one platform-wide credit token issued by Woodpecker (woodpecker@0a1b); not a proxy, not a fund (D10).

wpCredit is issued by Woodpecker. One ERC-20 for the whole platform, issued by the provider woodpecker@0a1b. D10 stands: it is not a proxy and not a fund; it is the credit that exchanges into every ServiceToken at that token's rate.
Steven, 2026-10-24: "wpCredit itself is issued by Woodpecker officially". How a wallet buys wpCredit from Woodpecker stays open.

D55

exchange(credits, to) sends the wpCredit to the token's owner and mints the service token to to; one direction only.

The credits of an exchange go to the token's owner. exchange(credits, to) on a ServiceToken pulls credits wpCredit from the caller, sends them to the token's owner (the developer wallet) and mints tokens to to at the token's rate. One direction only: nothing exchanges back into wpCredit.
Steven, 2026-10-24: "of course it goes to the token's owner". The caller approves wpCredit to the ServiceToken once, as it approves a token to contract A.

D56

Two ways to pay with wpCredit: buy the service token first (path A), or authorize one cap in wpCredit and let contract A settle in wpCredit at the rate stored at reserve (path B).

Both exchange paths exist, side by side. Path A, buy first: the wallet exchanges wpCredit for the service token, then authorizes and pays in that token as today. Path B, authorize in wpCredit: the cap is set in wpCredit, Intent.token is the wpCredit address, reserve locks wpCredit, and at settle contract A pulls actual × rate wpCredit and calls exchange(credits, payTo), so the service's payTo receives its own token; the Spent record keeps both amounts. The Console's "authorize with wpCredit" option becomes live.
Steven, 2026-10-24: "the cap can be done with wpCredit; path b and path a exist at the same time". The seams of the v1 spec §10 (wpCredit(), rate(), exchange(), the /pricing wpCredit field) are the implementation points.

D56a · D56 "Intent.token is the wpCredit address" → D56a (token = the service token, fund = wpCredit; one authorization in wpCredit per identity).

D56a

Path B uses the Intent's fund field (D7). token = the service's token (the pricing unit), fund = wpCredit (what the wallet pays). The authorization is (client, identity, wpCredit, cap): one record and one signature cover every service of the conversation. At reserve, contract A reads the service token's rate(), locks max × rate wpCredit and stores that rate in the reservation; at settle it pulls actual × rate wpCredit at the stored rate, calls exchange(credits, payTo), and Spent carries both amounts. The fund field leaves the D48 deferral; proxy tokens stay deferred.
Design seat, 2026-10-24, replacing D56's "Intent.token is the wpCredit address", which would not say which service token to exchange into. A wallet approves wpCredit to contract A once, as it approves any token.

D56b · D56a "calls exchange(credits, payTo) at settle" → D56b (A moves the credits to the owner and mints exactly actual to payTo; exchange is path A only).

D59 · D56a "one record covers every service of the conversation" → D59 (the token caps stay as ceilings; the wpCredit cap is the budget beside them; the Intent needs no fund).

D56b

Settle of a wpCredit-funded call (path B). Contract A pulls actual × rate wpCredit from the client to the token's owner (wpCredit.transferFrom(client, token.owner(), credits), at the rate stored at reserve) and mints exactly actual tokens to payTo through the token's existing mint allowance (setMintAllowance(A, …), granted by the owner at deploy; mint(payTo, actual)). The token's exchange(credits, to) is path A only and uses the current rate. The service always receives exactly actual tokens, the wallet pays at the stored rate, and a rate change between reserve and settle changes neither.
Design seat, 2026-10-24, from WoodpeckerMock3's finding on Change 39: had A called exchange at settle, the token would mint at the current rate and payTo would receive more or fewer tokens than actual after a rate change, and A would need its own wpCredit allowance towards the token. Follow-ups: the deploy flow's Contracts group gets setMintAllowance(A) (Console Change 37, the /deploy page); the map and /wpcredit wording (addenda to Changes 38–39).

D59 · D56b "pulls actual × rate wpCredit" → D59 (tokens first; only the shortfall is paid in wpCredit and minted).

D57

A token's owner can change its rate; the change applies to later exchanges, and a path-B call keeps the rate stored at reserve.

The rate of a ServiceToken can change. The token's owner sets rate() and may change it later (for example 2 tRunner = 1 wpCredit at the start, 3 tRunner = 1 wpCredit later); every change is an event. A change applies to exchanges after it: tokens already held do not change (tokens count, not price), and a path-B call uses the rate read at reserve, which the reservation stores, so settle never exceeds the Intent's max.
Steven, 2026-10-24: "this may change later". In the v1 POC rate() is a pure function returning 0, so a change would need a redeploy; D57 replaces that with a stored value. Accepted by Steven on 2026-10-24 ("the proposal of D57 is right"): keep the spec's unit ("wpCredit base units per 1 token") and give wpCredit 6 decimals, so cheap and expensive tokens fit one integer (2 tRunner = 1 wpCredit → 500000; 3 tRunner = 1 wpCredit → 333333; 1 tGpt = 2 wpCredit → 2000000) and settle multiplies without rounding; the Console shows the rate in Steven's words ("3 tRunner ≈ 1 wpCredit").

Amendments of 2026-10-24 (wpCredit in this version; Steven's answers)

D58

No milestones: the mock and the design description cover the complete program; wpCredit, both exchange paths and the changeable rate are in this version.

No milestones. This mock is the complete mock of the whole program, and the design description (the map, the walkthrough pages, this log, the v2 architecture document to come) describes the complete system. wpCredit, the exchange in both paths and the changeable rate are part of this version: the Console mock implements them (Change 37), the map and a walkthrough page show them (Changes 38–39). No "milestone", "later" or "POC placeholder" wording in the v2 documents or the UI.
Steven, 2026-10-24: "this version of the mock has to implement the rate … do not split it into so many milestones; this mock is the complete mock of the whole program, with the design notes". D27 (bundled deployment dropped) and D48 (proxy tokens and nested billing deferred) stand: Steven confirmed on 2026-10-24, "yes, this round's mock does not include proxy tokens or nested billing" — this mock has no proxy tokens and no nested billing.

Decisions of 2026-10-24, later (hybrid settlement; Steven's review of the New conversation dialog)

D59

Hybrid settlement: token caps are per-token ceilings, one wpCredit cap is the budget; settle takes the wallet's tokens first and pays only the shortfall in wpCredit.

Hybrid settlement. Two kinds of authorization work together on one identity: a cap per service token (the ceiling for that token — "tGpt at most 10") and one cap in wpCredit (the budget). For a call priced in tX, reserve needs remaining(tX) ≥ max; if the identity also holds a wpCredit authorization, contract A reserves max × rate on it as well (worst case) and stores the rate. Settle takes the tokens first: transferFrom(client → payTo) for what the wallet holds, up to actual; for the shortfall it pulls shortfall × rate wpCredit to the token's owner and mints shortfall tokens to payTo (D56b). used(tX) += actual; used(wpCredit) += shortfall × rate. Without a wpCredit authorization, settle fails when the tokens are short, as today. The Intent keeps its v1 fields: no fund is needed, A decides the funding from the authorizations it holds. Path A (buy first) is unchanged.
Steven, 2026-10-24: "add hybrid settlement … a service token's cap is there to limit that service token, for example tGpt allows at most that much". Feasible because reserve only books and settle moves the assets (v1 §3). Replaces D56a's "one record covers every service": the token caps stay, the wpCredit cap is added to them.

D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).

D67 · The /wpcredit note "D59–D61 will be re-stated on D64" → D67.

D59a

Switch off = exact mode. Without a wpCredit authorization the token caps are the only limits: a call stops when its token runs out, settle fails as today, and nothing is bought on the person's behalf. This is the mode for precise limits; the Console says so in one line next to the switch and warns when a cap exceeds what the wallet holds.
Steven, 2026-10-24: "this is right: with the switch off, it is computed from the caps that were set; for an exact limit, this is the way". No auto-buy anywhere.

D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).

D59b

Hybrid settlement, the details. One reservation record per call: token, max, the stored rate, and what it booked on the budget. At reserve, the budget booking is min(max × rate, the budget's remaining): a call the tokens can pay is never blocked by a small budget. At settle, the shortfall's credits must fit the budget booking, else settle fails, as a token-short settle fails in exact mode (D59a). Settle and release close the one record and free both bookings. The Spent event gains a last field credits (the wpCredit paid, 0 when the tokens covered the call).
Design seat, 2026-10-24, answering the gaps WoodpeckerMock3 found in Change 42.

D59b "one reservation record per call … the budget booking at reserve" and D60 "off → revoke(identity, wpCredit)" → pending re-statement on D64 (wpCredit is the next step; there is no reserve and no revoke).

D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).

D60

The Pay-in-wpCredit switch sits with the editable wpCredit cap in the dialog's budget block and in the conversation's authorization bar (authorize / revoke on the identity).

The wpCredit switch lives in two places. In the New conversation dialog, a budget block at the top holds the switch "Pay in wpCredit", the editable cap in wpCredit (default: the sum of the token caps at today's rates — the former "≈ wp" footer total) and the signature count; the per-token rows keep their caps. On the conversation page, the same switch sits in the authorization bar: on → the wallet signs authorize(identity, wpCredit, cap) with an editable cap, one transaction added to the identity; off → revoke(identity, wpCredit); the budget meter shows used / cap in wpCredit.
Steven, 2026-10-24, points 1 and 3: the total and the switch "they are too far apart, I did not understand it"; the conversation page "there must also be a pay in wpCredit switch". Contract A needs nothing new for the toggle: authorize / revoke on an existing identity exist in v1.

D59b "one reservation record per call … the budget booking at reserve" and D60 "off → revoke(identity, wpCredit)" → pending re-statement on D64 (wpCredit is the next step; there is no reserve and no revoke).

D59 / D59a / D59b / D60 (hybrid settlement with the budget booked at reserve; off → revoke) → D67 (the shortfall is funded at settle at the current rate; off → authorize with the budget = used).

D61

Each per-token cap row reads cap → what you hold → the rate (1 wpCredit = N tX) → the ≈ wp value, in plain layout.

The cap cell says what it means. Each per-token row shows the cap input, what the wallet holds, the rate as "1 wpCredit = N tX" and the ≈ wp value, laid out so the eye reads cap → holds → rate → value; no monospace run of five facts in one line.
Steven, 2026-10-24, point 5: "there is already a rate here, only the UI design is wrong, not friendly at all". Design by WoodpeckerMock2 (Change 41).

D67 · The /wpcredit note "D59–D61 will be re-stated on D64" → D67.

Decisions of 2026-10-26 (from the billing discussion with Steven)

D62

1 wpCredit = 1 USD; a token's rate is a dollar price per token, derived from the service's cost per billable unit.

1 wpCredit = 1 USD. Woodpecker issues wpCredit at a fixed one-dollar value. A service token's rate ("1 wpCredit = N tX") is therefore a dollar price per token, and a developer derives it from cost: revenue per billable unit ≥ upstream price + hosting share + gas share.
Steven, 2026-10-26: "by default 1 wpCredit equals 1 USD for now". Replaces the sample rates of the mock as the basis; the mock's numbers are not updated by this decision.

D63

The runner's billable unit is one round (1 tRunner); the default cap is 2048 tRunner per conversation, from real usage data; defaults live in the service's price document.

The runner's default cap is 2048 tRunner per conversation. The runner's billable unit is one round (one model generation with its tool calls), 1 tRunner each; a turn has as many rounds as pi's loop allows. Basis: the Engine's real data (99 conversations, 205 turns: median 2 rounds per turn, max 34) and this design session (504 rounds in two days); 2048 covers a heavy two-day conversation. Default caps per service are published in the service's price document (its hash is committed on chain) and overridden by the person's settings record; a new wallet uses the published defaults.
Steven, 2026-10-26: "let the runner's default be 2048". Supersedes D43's sample "max 2 tRunner per turn". Defaults for llm and memory are still to be set the same way (from data).

Decisions of 2026-10-27 (the payment logic, confirmed by Steven)

D64

Pay as you go on the service tokens: contract A keeps cap and used per wallet, agent and token; the service writes receipts and settles batches with one agent-signed Intent; no reservation, expiry or revoke; a failed settle pauses the conversation until the cap is raised or tokens are bought.

Pay as you go on the service tokens: cap and used, receipts, batch settle, no reservation. A service token is a prepaid balance for one service; the wallet holds it. Contract A keeps two numbers per (wallet, agent, token): cap and used. The wallet sets the cap with authorize(agent, token, cap): three in one multicall at a new conversation, one more to raise or lower it later; any value is allowed (a cap below used only means nothing more settles); no expiry. The service does its work and writes one receipt per piece of work (runner: one round = 1 tRunner, D63; memory: one commit = 1 tMemory; llm: one call, metered by tokens), then settles in batches of its own size: it hands the batch's receipts to the wallet's DO, the DO checks the sum and signs one Intent {client, identity, token, payTo, max, nonce, context} with the conversation's agent key (max = the sum, context = the hash of the receipts), and the service's submitter calls settle(intent, signature, actual) with actual ≤ max. Contract A checks the signature, the nonce and used + actual ≤ cap, adds actual to used, transfers actual tokens from the wallet to payTo and emits Spent. A failed settle (cap reached, or the wallet short of tokens) stops the service: it withholds the unsettled result and the conversation pauses until the person raises the cap (one authorize) or buys tokens; the submitter then resends the settle. The batch is the service's maximum loss (zero at batch size 1). A service that pays an upstream first guards its own bound in its own code (the llm gateway clamps max_tokens to what cap − used can pay); the protocol does not. The Console checks balance ≥ cap per token when a conversation is created; conversations share the wallet balance, so the check holds at creation only. The DO's duties: derive the agent, sign the batch Intents, record each batch's settle state; a look at cap − used − unsettled before a round is optional and only avoids wasted work. To stop a conversation the Console stops the DO from signing; on chain, authorize with cap = used has the same effect. Contract A has no reserve, release, revoke, expiry or grace period; the protocol has no reservation, voucher, plan, boundary policy or extension; "billable unit" is only the unit of a price rule, not a step of the flow.
Steven, 2026-10-26/27: "we do not need it that complicated: New conversation checks whether the wallet's service tokens cover the cap, then every finished call deducts a little at a time; several conversations stacking on each other is fine"; "there can be no locking now, and no expiry either"; "ok, this used is required"; the final 40-line text confirmed on 2026-10-27 ("I confirm this version"). Replaces the reservation model of docs/BILLING-FRAMEWORK.md (first draft, 2026-10-25) and contract A's v1 reserve/release/revoke/expiry (wpDurable/docs/SERVICE-TOKEN-ARCHITECTURE.md §3). wpCredit is the next step: D54–D58 stand (issue, exchange, rate, path A); D59–D61 (hybrid settlement with the budget booked at reserve, the switch, the cap cell) are stated on the reservation model and will be re-stated on D64 in a later decision; until then the Console mock and the /wpcredit page keep them, with a note. Changes 46 (BILLING-FRAMEWORK.md, WoodpeckerMock2) and 47 (the map, WoodpeckerMock3); the base-service notes by the design seat.

D66 · D64 "the batch is the service's maximum loss (zero at batch size 1)" → D66 (a service never delivers before its settle; the loss is upstream cost and compute only).

D67 · The /wpcredit note "D59–D61 will be re-stated on D64" → D67.

Decision of 2026-10-29 (the map, from Steven's review)

D65

No Developer Portal in v2: the Console holds its deploy pages and the service statistics; the map shows no Portal box.

No Developer Portal in v2. The v1 Developer Portal is gone: its deploy pages are the Console's (D14), a service's statistics are on the Console's manage page (D29), and it has no wallet. The map shows no Portal box and its intro does not mention it.
Steven, 2026-10-29, circling the box on the map: "this developer portal no longer exists". Supersedes D14's "the v1 Portal is not merged (stays beside)". Change 48 (WoodpeckerMock3).

Decisions of 2026-10-29, later (settle before deliver; hybrid settlement re-stated)

D66

Settle before deliver: a service delivers a result only after its batch has settled; a failed settle delivers nothing; batches exist for performance only and are set at deploy (1, N, or per turn for a runner).

Settle before deliver: the highest principle. A service delivers a result only after the batch that contains it has settled on chain. If the settle fails, nothing of that batch is delivered. A service's loss is therefore never a delivered result: at most the upstream cost and the compute of its open batch. Batches exist for performance only (fewer settles, less gas); a service that accepts no loss at all sets its batch to 1. The batch is a field of the service's price document, set in the Console's deploy form, row "batch": 1 = one settle per receipt (the default for model and tool gateways); N = one settle per N receipts, results held until the settle passes; "per turn" for a runner = the turn's rounds in one settle before the turn's answer. Consequences: a model gateway's batch is 1 in practice, because the DO needs each answer to continue the round, so each model call waits one settle (2–6 s on Base) before its answer returns; the runner settles the turn's rounds before the answer goes to the Console; the memory service keeps a commit pending until its settle passes and answers a read or a search only after its settle; the person never sees an unsettled spend (every chip is settled); the DO's optional look before a round stays optional.
Steven, 2026-10-29: "every interaction settles first and delivers after, so if the settle fails nothing is delivered, the whole batch is not delivered … if I do not get the money I do not deliver the result; the most that is lost is the upstream cost, or the compute already spent on the work … delivering first is the loophole. This is the highest principle. Batches exist; their value is performance; whoever fears a loss can set the batch to 1". Supersedes D64's "the batch is the service's maximum loss (zero at batch size 1)" and the framework's "deliver, or withhold" choice: holding the result until the settle passes is mandatory. Changes 49 (the map), 49a (the framework document), 50 (the Console).

D67

Hybrid settlement on D64: the budget is an authorization in wpCredit; at settle the tokens go first and the shortfall is paid in wpCredit at the current rate and minted to payTo; switch off = budget set to used.

Hybrid settlement re-stated on D64 (replaces D59, D59a, D59b, D60 as far as they relied on reserve and revoke). The budget is one more authorization: authorize(identity, wpCredit, budget); switch on = this record with budget > 0; switch off = authorize(identity, wpCredit, used) (nothing more is covered; no revoke). At settle(intent, signature, actual) for a token tX contract A takes the tokens first: have = min(balanceOf(client), actual), transferFrom(client → payTo, have); the shortfall actual − have is paid in wpCredit when the identity holds a wpCredit authorization: credits = shortfall × tX.rate() at the current rate (no reservation, so no stored rate), used(wpCredit) + credits ≤ budget else the settle fails, wpCredit.transferFrom(client → tX.owner(), credits), tX.mint(payTo, shortfall) under the mint allowance (D56b); used(tX) += actual; used(wpCredit) += credits; Spent(client, identity, token, payTo, actual, nonce, credits). Without a wpCredit authorization a token-short settle fails as in D64 (exact mode, D59a). A gateway's own guard reads both remaining values before it works. Path A (buy first, D55) is unchanged. The person's exposure to a rate change is bounded by the budget.
Design seat, 2026-10-29, for the two branches of the failure scene Steven asked for ("complete the 20-step version and give it two branches: one with wpCredit on, and a few with wpCredit off"). D61 (the cap cell) stands.

Decisions of 2026-11-03 (extensions)

D68

Extensions: a page that runs in the Console in its own tab, an iframe from the extension's own origin, reaching the Console only through the extension API over postMessage; it never holds the wallet, a credential or a key.

An extension is a page that runs in the Console in its own tab. The tab is an iframe served from the extension's own origin: one subdomain per extension on the host service (D71). Extensions are independent of services: an extension may use several services, several extensions may serve one service, and an extension may use none. The whole tab is the extension's: its own DOM, any framework. It reaches the Console only through the extension API over postMessage (D76); it never holds the wallet, a session credential or an identity's key, and it never touches the Console's DOM. Only two things go through the Console: signatures, through the wallet's signing panel (D32, D45), and paid writes, signed by the extension's identity (D73).
Designed with Steven, 2026-11-02/03; built into the Console mock as Change 52 (four sample extensions: the Data Vault manager, Devices, Notes and the Film editor). The Console keeps the frame around the tab: the identity, the caps, the actions and the log of the extension API; the page inside is the extension's own.

D69

The menu: a fourth item, Extensions (My extensions, the Directory, Publish an extension), and one item per installed and enabled extension; the person may unlock, drag and lock the order, which lives in the browser.

A fourth menu item, Extensions. Its page has three parts: My extensions (the installed ones, and this wallet's own with Manage), the Directory (other owners' published extensions, with Install) and Publish an extension. Each installed and enabled extension is a menu item at the same level as the others: its tab. The person may unlock the menu, drag the items into an order and lock it; the order lives in the browser (D50). D12 is amended (D12a).
The default order is Conversations, Services, Extensions, the extensions' tabs, Settings. A disabled extension leaves the menu and keeps its caps (D72). Mock routes: #/extensions, #/extensions/publish, #/extensions/<id>, #/extensions/<id>/manage.

D70

ExtensionDirectory: a second minimal contract, publish(name, url, hash) and unpublish(name) by the owner wallet; an entry holds owner, name, url, hash, updatedAt and active; the handle comes from the ServiceDirectory. Decided, not on chain yet.

A second minimal contract, the ExtensionDirectory: no admin, no fee, no upgrade. Writes by the owner wallet: publish(name, url, hash) and unpublish(name). An entry holds the owner, the name, the url, the hash (sha256 of manifest.json), updatedAt and active. Reads: entry(owner, name), entriesOf(owner), count() with enumeration (entryAt(index)). The handle is read from the ServiceDirectory (handleOf), so an extension's id is handle/name, for example steven@68df/film-editor; a wallet without a handle claims it there first. Events Published and Unpublished. Decided, not on chain yet.
Sample address in the mock: 0xe3d7…40b2. The chain holds only the url and the hash; the code is at the url (D74). Kinds, prices and permissions are not in the contract: they are in the manifest (D77). The owner wallet pays the gas of publish and unpublish.

D71

Publish: a zip with manifest.json, read and hashed in the browser, stored and served by the host service (one paid operation by the Console's identity), then publish(name, url, hash) signed by the wallet; a new version is a new publish and installed copies keep their pinned hash.

Publishing is a form, not an agent conversation. The package is a zip with manifest.json at its root. The Console reads it in the browser: the manifest, each file's sha256 against the manifest's files, and the manifest's own sha256. The host service (kind host) stores the package and serves it from one subdomain per extension and one path per version (https://<name>.<handle>.ext.<host>/v/<hash>/), with the response headers the frame needs (frame-ancestors the Console, a CSP baseline, CORS on the manifest); the upload is one paid operation in tCloud, paid by the Console's identity (D75). Then the wallet signs publish(name, url, hash) (claim the handle in the ServiceDirectory first when the wallet has none). A new version is a new publish: the chain holds the new hash; installed copies keep the hash they pinned and show that an update exists; a changed permission asks for consent again. The host keeps old versions (how long: in its price document, open).
Sample: steven@68df/film-editor 0.2.0, manifest hash 0x2949…f843, url https://film-editor.steven-68df.ext.woodpecker-0a1b.workers.dev/v/2949f843/; the upload costs 5 tCloud on woodpecker@0a1b/cf (metered, ≤ 5 per operation). The signing panel reads “Publish steven@68df/film-editor 0.2.0 · 1 transaction”. Walkthrough: publish.html.

D72

Install: the card shows what the manifest declares, a Store select when it needs one and one cap per token for the extension's identity; one wallet request (approve on first use, one multicall of authorize), then the install record is committed to the settings record; uninstall sets every cap to used.

Install, from the Directory or from My extensions. The install card shows what the manifest declares (configuration quota, store need, services called, wallet permission, reads, notifications), a Store select when the manifest needs one (a service of kind store from My services; there is no default store), and one cap per token the extension's identity may spend: its configuration's tMemory, its store's token, the token of each service it calls, with the balance ≥ cap gate of D64. Install asks the wallet once: approve(A) on each first-used token, then one multicall of authorize for the extension's identity; then the install record (id, version, hash, identity, store, caps, enabled) is committed to the settings record, a commit by the Console's identity (D75). Disable keeps the caps and does not load the tab; uninstall sets every cap to used (one multicall) and removes the install record; the data in the store stays. An extension's calls to a service are paid by the extension's identity within these caps.
Signing panel: “Allow the extension Film editor to spend up to ≈ 10.5 wp · 10 tMemory · 50 tDatavault” (approve tDatavault on its first use, then authorize ×2 in one transaction); “Uninstall Film editor · 2 authorize with cap = used · one transaction”. Walkthrough: extension.html.

D73

Identities in the browser: the Console's identity and one identity per installed extension are derived from a root the wallet signs once; they sign Intents and sign-in challenges in the Console; a service accepts an identity that contract A shows authorized by the wallet (D47a).

The Console's own identity (one per wallet) and one identity per installed extension are derived in the browser from a root the wallet signs once: a deterministic signature over a fixed message (EOAs qualify; smart-contract wallets are an open item). The Console signs their Intents and the services' sign-in challenges with these identities; the keys never leave the Console's origin. A service accepts a sign-in by an identity that contract A shows authorized by this wallet (D47 amended: D47a); a brand-new wallet signs in with the wallet itself the first time, as D47 says today. The runner keeps its root and the agents: D2 unchanged; no DO interface signs for the Console or an extension; signing costs nothing. No conversation stands behind an extension: an extension that needs an agent is a v2 item (conversation methods on the extension API).
Sample identities: the Console's 0x0c71…a3e9; the Data Vault manager's 0xe7a1…4c09; Devices' 0xe8d2…51b3; the film editor's 0xe9f3…6e27. A conversation's identity stays the agent, derived by the runner's DO (D2). The wallet's Authorizations tab shows one group per identity: the conversations, the Console, each extension, each device (D78).

D74

Where extension data lives: code at the url and the hash on chain; install records in the settings record; configuration in the memory service, one namespace per extension; large data in a store service chosen at install; cache in the browser; on chain only the caps.

Where extension data lives. Code and manifest: at the url the directory holds, the hash on chain. Install records: the settings record in the memory service. Configuration: the memory service, one namespace per extension (ext:<id>), bound to the extension's identity at its first write, 1 tMemory per commit, within the manifest's quota. Large data: a store service, a new kind store (paid per MiB started, reads free for the owner), chosen by the person at install; no default store. Cache: the browser. On chain: only the caps. D3, D34, D50 and D51 are amended (D3a, D34a, D50a, D51b).
Sample store: steven@68df/datavault, kinds store and tool, token tDatavault, fixed 1 per MiB started, 1 wpCredit = 100 tDatavault; the wallet holds 100. The install record pins version, hash, identity, store and caps, and carries the enabled flag (D72).

D75

Paid writes outside a conversation: the settings record's commits and a publication's upload are paid by the Console's identity, whose cap is asked in the signing panel on first use and raised when it is used up; a write is confirmed only after its settle.

Paid writes outside a conversation are paid by the Console's identity: the settings record's commits (every change of a default, every install record) and a publication's upload. A commit is 1 tMemory; the upload is one host operation in tCloud. The identity's cap for a token is asked in the signing panel the first time a write needs it (approve on first use, then authorize) and raised when it is used up. A write is confirmed only after its settle (D66); a failed settle stores nothing. Reads are free for the owner.
Signing panel: “Allow the Console to spend up to ≈ 5 wp · 5 tCloud · for the upload of an extension package”; the other reasons read “for the commits of your settings record”, “for the install record of an extension”, “for the settings record after an uninstall”. Settings shows the record: the memory service, the Console's identity, “tMemory used 2 of 20”, the number of commits so far.

D76

The extension API, v1: tabs only, over postMessage; ready, config, store, services.call, wallet.request, ui.toast and the reads, with permissions from the manifest and six error codes; a paid write settles on the extension's identity before the reply.

The extension API, v1: tabs only; slots inside the Console's own pages are v2 on the same API. Over postMessage: requests { wp: 1, id, method, params }, replies { wp: 1, id, ok, result | error }, events { wp: 1, event, data } (theme, caps, lang). Methods: ready; config.get, config.put; store.put, store.list, store.delete; services.call(service, tool, args); wallet.request(kind: authorize, identity, label, token, cap, reason); ui.toast; read.balances; read.authorizations. Permissions come from the manifest (storage.config, storage.store, services, permissions.wallet, permissions.read, permissions.notify); error codes permission_denied, cap_exceeded, balance, no_store, rejected, unknown_method. A paid write settles on the extension's identity before the reply. wallet.request goes through the signing panel, which names the extension, the identity to authorize and the reason; the record on contract A is for the named identity (a device), not for the extension. The Console keeps a log of the calls on the tab.
SDK woodpecker.js: Woodpecker.connect() sends ready and keeps its result; wp.call(method, params); wp.on(event, handler). ready answers with the wallet, the extension (id, name, version, identity), the language, the theme, the permissions, the caps, the store and the services with their prices. A reply to a paid write carries the receipt (token, actual, tx) and the caps after it.

D77

The manifest: id (handle/name), name, version, description, icon, api, entry, files with sha256, services, storage quotas, permissions (wallet, read, notify) and locales; no content scripts, no access to the Console's DOM.

**The manifest, manifest.json**: id (handle/name), name, version (major.minor.patch), description, icon (a lucide name, D39), api (1), entry, files (path, sha256, size), services (names), storage (config quota, store quota), permissions (wallet with authorize, read with balances and authorizations, notify), locales (en, zh-CN, es-ES; identifiers never translate). No content scripts and no access to the Console's DOM.
The film editor's manifest: entry index.html, storage config 8 KiB and store 256 MiB, permissions notify, no services. The Devices manifest: wallet authorize, read authorizations, notify. The Data Vault manager's: services steven@68df/datavault, config 4 KiB, read balances, notify.

D78

Devices: a device has its own identity; an extension with the wallet permission asks the person, through the signing panel, for a cap on it; the device signs its own Intents and services settle against that record; Stop on chain (cap = used) revokes it.

A device has its own identity: its own key, or a key a devices service keeps for it (open). An extension with the wallet permission asks the person, through the signing panel, for a cap on that identity; from then on the device (or its service) signs its own Intents and services settle against that record; Stop on chain (cap = used) revokes it. Where a device's logic runs is open: the device itself calling a model service; a devices service running a fixed pipeline per device with the device's key; or an agent conversation the device posts into (which needs D47's amendment and a rule on which conversation a device may post to).
Sample: the Devices extension (woodpecker@0a1b/devices, identity 0xe8d2…51b3) asks for kitchen-cam, 0xd3c1…ce01: “Give kitchen-cam a cap of 20 tVision · asked by the extension woodpecker@0a1b/devices · stream clips to the agent”; approve tVision on first use, then authorize(0xd3c1…ce01, tVision, 20); the wallet's Authorizations tab gains the group Device · kitchen-cam.

D79

Security of the extension model: EIP-4361 challenges, short-lived per-service credentials, no credential or key in an extension, the hash pinned at install and checked against the chain, namespaces bound to one identity, the signing panel names the extension; open: file verification at load, encryption, smart-wallet signatures, quota enforcement.

Security of the extension model. Challenges follow EIP-4361 (domain, nonce, expiry); a session credential is per service and short-lived; an extension never holds a credential or a key; the install record pins the hash and the Console checks the manifest's hash against the chain at install and at restore; the memory service binds a namespace to one identity; the signing panel shows the extension's id and the reason on every request it asks. Open: verifying the files at load (a service worker in the extension's origin); encryption of the settings record and the configuration namespaces (the memory operator reads plaintext today); deterministic signatures for smart-contract wallets; quota enforcement by the memory service.
Open items of this section, collected: who runs a device's logic (D78); file verification at load; encryption; smart-wallet signatures (D73); the memory quota rule; the host's retention of packages (D71); the first wallet's gas and the starter pack trigger (already open, D44).