Repository navigation
Paid runs 8: agents and the store — /public/mcp tools, llms.txt, prices in live items, the store's Run form - #620
Merged
Conversation
Evanfeenstra
force-pushed
the
feature/paid-runs-8-agents
branch
from
November 11, 2026 04:20
077d4dc to
edbccc7
Compare
…view, prices in the index's live items list_public, run_workflow, call_tool, get_run and get_credit beside a visitor page's step_run, over the same doors as the HTTP routes (PublicDoors gains call, run, credit, claim, runnable, listing). An unpaid priced call is a payment_required result; payment or the connection's L402 header pays; a macaroon alone claims. A launch carries a key (the agent's, else one from the workflow and input) so a retry is the same run while in flight. Long runs answer a handle get_run waits on, with progress. GET /llms.txt is open: the index's page (with a run section when it sells), else the public view's. The index's live crawl keeps each server's runnable items, checked, and GET /index/live carries their prices.
A live workflow or step anyone may run gets its price card and a Run (Call) form drawn from its input on its store page. The launch goes to the publisher from the visitor's browser with no credential; one key per press, resent on every retry. A 402 shows the PaymentSheet, the paying loop is the visitor's callPaying over the HTTP answers, and the credit and every invoice shown live per publisher in sessionStorage, claimed until settled whether or not the sheet is open. The runs a tab launched are read back by their run tokens until they end. The agent tools yield progress every 5 s: the door sends a yield's progress when the next one arrives, so the first notification lands 10 s in. specs/API.md §17.8, the store paragraph, /index/live's run; AGENTS.md; the plan's status line.
…free default key, the door's own invalid_token only, nothing paid cut, llms.txt cached - GET /public/credit (and get_credit) lists the runs and calls the token paid for, each run with its run token: after a lost answer an agent finds the run it started instead of paying for another. Every tool says so; a launch's first progress note carries run_id and token. - A priced launch's default key is made by the launcher from the PARSED input, keys sorted (PublicLaunchRequest.defaultKey); a free launch gets a key only when the agent passes one (its scope is a shared address). - Every agent tool answers within 45 s (wait <= 45): a launch still starting, a run still going (its handle) or a call still going (still_running, check get_credit) answers what it has. - No builder cap: a call's output comes back whole; a run's output over 100k chars is left out with the run's url (token on it). - A run reads as going by its own status; ended with its charge pending says charge_pending (the launcher's view waits at most 2 s for the charge). - refusesCredential: only the door's own refusal (code invalid_token + status 401, else an error that begins invalid_token:); a step's failure that mentions it keeps the credential and is not sent again. - GET /llms.txt: built at most every 30 s per base URL, counted against the /public budget, no /public/mcp lines when the MCP doors are off. - The store: charge pending shown; Your credit lives in this tab: show and copy the credential, paste one back.
Evanfeenstra
force-pushed
the
feature/paid-runs-8-agents
branch
from
November 11, 2026 04:26
edbccc7 to
c9d7730
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work item 8 of
plans/paid-runs.md, "Agents and the store" (§10 "An agent over MCP" and "The store").What it does
An agent over MCP (
src/public-agents.ts).POST /public/mcpnow serves five tools beside a public app page'sstep_run.step_runis unchanged and still app-only.list_publicGET /public'srunnable), pluscall_withrun_workflowPublicDoors.launch, the same function the HTTP route uses, so admission, budgets, caps, price, key and drain are one code path. It waitswaits (25 by default, 45 at most), then answers a handle{ run_id, token }withnext. The run's first progress note already carriesrun_idandtokencall_toolPublicDoors.call: the calls' per-address budget, the drain, the body, thencreatePublicTools().call. The HTTP route is now a thin wrapper over itget_runget_creditruns: the runs and calls it paid for, each run with its run token. Given the macaroon alone, it claims the invoice instead{ payment_required: { macaroon, invoice, usd, sats, rate, description, expires, remaining_usd?, reason?, carries? }, next, key? }. The agent pays and calls again withpayment: "<macaroon>:<preimage>". The connection's ownAuthorization: L402header works too.key, or else one the launcher hashes from the input as the workflow'sinput:block parsed it, keys sorted. A retry while the run is in flight, after paying or after a lost answer, returns that run and never sets aside a second reserve. A free launch carries a key only when the agent passes one, because its scope is the caller's address, which a hosted agent's users share.get_credit. A key matches only a run in flight (Paid runs: a key dedupes a run in flight only, a charge never passes its reserve #615), so once a run ended, the same call would start and pay for another.get_credit(andGET /public/credit) lists the runs and calls the credential paid for, each run with its run token, and every tool's description says to read it before calling again after a timeout or a dropped answer.still_running, "check get_credit") answers what it has. Progress also goes to a client that passed aprogressToken.url, which carries the token. Every run answer carries thaturl.charge_pending, instead of looking like it's still running.PublicDoors(src/public-routes.ts) gainscall,run,credit,claim,runnableandlisting. The routesPOST /public/steps/:type/run,GET /public/credit,POST /public/credit/claimandGET /publicnow call them.GET /llms.txtis open whatever the role.# Run on this serversection when it also sells.404./publicbudget. The/public/mcplines are left out when the MCP doors are off.Prices in the index's live items (
src/packages/index.ts). The live crawl keeps each server'srunnable, checked as a stranger's answer (liveRunnableOf: finite numbers ≥ 0, strings cut, at most 500 items).GET /index/livecarries an item'srun: { base, markup, capUsd, reserve, results, recent? }. A runnable item whose definition isn't public is an item of its own withdefinition: false.The store's Run form (
web/src/components/Store.tsx,web/src/store-run.ts,web/src/store-purse.ts).input:block throughbindingsFromInputBlockandConfigField, or from a step's JSON Schema.key, which is sent again on every retry, including after a dropped request. A call is never sent twice.PaymentSheet. The paying loop is Paid runs 10: a visitor's app page #613'scallPaying, over the HTTP answers (one new optionallaunchfield).sessionStorage, never in a URL. One claim keeper per publisher claims each invoice until it settles or ends, whether or not the sheet is still open.GET /public/credit). That's how credit survives the tab.refusesCredentialfix, which is also on main since #613. It only matches the door's own refusal: a result'scode: invalid_tokenwithstatus: 401, or else an error that begins withinvalid_token:. A step's failure whose text merely mentions it (an OAuth error, already charged) keeps the credential and is never sent again. It used to drop the credential and resend, which meant a second sheet and a second execution if paid.Departures from the plan
payment: "<macaroon>"with no preimage claims the invoice first, onrun_workflow,call_toolandget_credit. Once it settles, the answer carries the wholecredential. This is so credit a person paid from a phone is never stranded for an agent.src/ai/mcp.tssends a yield's progress only when the next yield arrives (it can't tell which yield is the last), so the beat is 5 s and a launch yields its handle first.GET /public/credittoo, not just inget_credit. The credential holder launched those runs, so it may hold their tokens.get_runreads by run token only. It doesn't read a public run that has no token.GET /llms.txtis open on every strut. A strut with nothing public answers404where it used to answer401.index.test.tsand the gate sweeps are updated.input. The store reads it fresh from the publisher's/public. The index's ownllms.txtand/index/mcpdon't list live items' prices yet; only/index/liveand the store do.Tests
src/public-agents.test.ts(new, 14 tests), againstFakeWallet:step_runstill app-only;list_public;payment_required, then paid, then the balance read on another connection;call_tool;llms.txt;src/packages/index.test.ts: a live row'srunnablewith prices, and junk from a stranger's server dropped.web/src/store-run.test.ts(new): price words, fields, values, HTTP answers into the paying loop (402 → pay → the same key resent; refused after paying keeps the credit; cancelled), the run list.get_credit;llms.txtwith MCP off;refusesCredential(visitor-pay and store-run).node@22 --import tsx --test): 290/290.npm teston Node 24: 4234 pass. One failure,index.test.ts"a publisher asks, the operator approves…", was anENOTEMPTYwhen a row's removal raced a crawl's page write under full-suite load. It passes 4/4 alone, and that path isn't touched here.Browser check. I ran a scratch index strut serving the store and a publisher strut with
FakeWallet, in the built-in pane. Each of these worked as expected:Real MCP client. I used the MCP SDK against the same publisher:
Not tested
FakeWallet./labgate. That gate passesPOST /public/mcp, so the agent tools work through it, but it doesn't passGET /llms.txt, which isn't under/public. That needs a change in stakgraph.