Changelog
All notable changes to this project are listed here. The format follows Keep a Changelog, and versions follow Semantic Versioning. From 1.0.0 on, a breaking change comes only in a new major version (2.0, then 3.0); before 1.0.0, a minor version could change interfaces.
[3.0.0] — 2026-11-06
Breaking changes (3.0)
Five changes need action from some users; each but the first has an option that keeps the 2.x behaviour. What to change, who is affected and what does not change: Upgrading to 3.0.
- Node.js 22 or later: every package declares
engines>=22; CI tests Node.js 22 and 24. No opt-out: stay on 2.x. createAIProxyrefuses a metered path of a formatformatsleaves out (HTTP 400bad_path, not forwarded) instead of passing it on without a receipt (refuseOtherFormatsdefaults totrue). Opt-out:refuseOtherFormats: false.createMcpProxyrefuses a browserOriginnot inallowedOrigins(HTTP 403, preflight included); clients that send noOriginare not affected, andcreateMcpEndpointis unchanged. Opt-out:allowedOrigins: '*'.tapeapi-verifyis strict by default: a receipt that does not verify is an error to the client (HTTP 502, or an error event at the end of a stream). Opt-out:--no-strict.createMcpProxyandtapeapi-mcpnegotiate the upstream revision (upstreamNegotiation: 'auto':server/discoverfirst, theninitialize). Opt-out:upstreamNegotiation: 'legacy',--upstream-negotiation legacy.
Not a change, a longer promise: conform: false (the 1.x resolution, deprecated since 2.0) is kept through all of 3.x and removed no earlier than 4.0; its one DEPRECATED warning per client now says so (it said "kept in 2.x").
Experimental changes
Everything below is experimental, outside the compatibility promise; the 2.0 default resolution (TAP-10 / TAP-11 §2) is unchanged. In eight groups: the Agent Passport and policy circuits (3.0); interop and the operator kit; the client-side budget and spend statement; bounded agents before any audit; hardening from internal reviews; an x402 batch-settlement enforcement backend; spec notes and receipt references (a proposal); docs. The escrow that paid calls rely on is still not audited and not deployed, so nothing below that pays works on mainnet.
3.0: Agent Passport and policy circuits (experimental)
@tapeapi/passport0.1.0, a new package next to the SDK: a read-only resolver for Agent Passports, following an open draft, TapeOutProtocol/TAPs pull request #83 at headcae25e6(not merged; every result carries that head, and the package records it with the SHA-2567720772c…fca4of the draft's vectors). The draft has been updated since (nowcbf4e03): the package implements the text atcae25e6, and a package that follows the new text is planned. It decides, at one pinned block under strict agreement, chain first and files last, whether one wallet holds both an ERC-8004 agent identity and a TapeOut circuit and published the link, and answers with one state word (CONFIRMED, BROKEN, AMBIGUOUS, wallet-unset, stale-agent-wallet, UNAVAILABLE), one named reason and every warning. It interoperates by reading ERC-8004's deployed public registry on BNB Smart Chain; it is not an extension of ERC-8004 and is not endorsed by its authors. Nothing on mainnet is known to resolve to CONFIRMED yet. The registries are upgradeable (UUPS; the owner is a proxy shaped like a 3-of-5 Safe): an unknown implementation makes every result UNAVAILABLE. Gifted identities and circuits do not resolve as passports. Without log-capable nodes, site operators and the holder history are reported as not checked, never as passed. Holders whose wallet signs EIP-712 only have no sealed inbox, which is not a passport failure. Also: the registration file written from chain reads, an A2A card for CONFIRMED only (display only, never a source of signers), and the optional./feedbacksubpath (§10, conditional on the unmerged review draft #54). The draft's 147 vectors pass as a pinned test fixture (not in the package); TapeAPI's own extra cases (capital look-alikes, decimal literals in a registration file) are kept apart. In the SDK: the experimentalapi.chain.processorNumber, and the commandstapeapi passport,tapeapi-doctor --passport(one check after manifest-format, not one of the 14),tapeapi connect --passportandtapeapi init --passport(writes.well-known/agent-registration.jsonlocally after read-only checks and prints the holder's ownputFilesteps; it sends nothing). They load the package only when asked and say how to get it when it is missing. The holder console (the onetapeapi initpoints to) proposes a 30-day delegation instead of 90 days, W-1's default limit; the holder can type any whole number of days from 1 to 366 (7 and 90 included).tapeapi-doctorwarns from 7 days left instead of 30, and so do the new-api sidecar'sRENEWline and its daily check (new--delegation-warn-days/--delegation-escalate-days, 7 and 3). Guide: Agent Passport.- New package
@tapeapi/policy-circuit0.1.0 (experimental, next to the SDK, never part of it): policy circuits after the Policy Circuit Gate TAP draft (TAPs pull request #84, a conditional draft, submitted, now a draft PR, not merged; followed at its head0b73174: a class and a unit per asset in the tables, and an asset the tables do not list overflows). A spending rule written as atapnet/1circuit (128 input pins, 8 output pins, at most 49,152 bytes) is re-run over a 128-bit request word built from the request and the signed tables (amounts counted in the request's own asset's unit, rounded up and saturated; decimal strings for integers above 2^53, an unsafe JS number refused);PolicyBindingand its revocation are checked in TAP-11's EIP-712 domain on the SDK'ssigandcanon(checkPolicyBinding,verifyPolicyBinding; revocation signers recovered, not taken as given; contract binders only through an EIP-1271 reader at a TAP-10 pinned block, otherwisebinderChecked: false); andcreatePolicyGate().admit()gates a service's requests and keeps a trace that replays, failing closed in every row of the draft's failure table. Fixed in code: bindings to a mandate (TAP-18, from #47, merged as a Draft; merging is not adoption) or an escrow session (#59, #60) areconditionalat best, never enforceable (enforceablereports the signer's authority, never that anyone enforces the binding); labels are display only; no file, no verdict; an x402 batch-settlement channel is never an escrow-session subject (policy-subject-x402), and a gate for an escrow-session binding needs the session (policy-session-required). The gate reads the binder again before every decision, so a transfer of the circuit ends the earlier holder's bindings at the next decision. It is an off-chain check that binds only the parties that check it, not a signing guardrail; nothing is deployed for it and it sends no transaction. Tests: all 188 cases of the draft's vectors (a copy pinned by SHA-256, not in the package), the checks outside them, the enforceable ladder over every binding vector and random bindings, and a differential test against the draft's Python TAP-02 reference (whenpython3is present). Not in it: a budget-to-circuit compiler and a server hook. Guide: Policy circuits (en, zh-CN). - The 0.x packages' versions (release rule):
@tapeapi/interop,@tapeapi/passportand@tapeapi/policy-circuitkeep versions of their own, and one also takes a new 0.x version whenever its@tapeapi/sdkor@tapeapi/serverrange changes, so two releases never carry a tgz of the same name and version with different contents.
Added: interop and the operator kit (experimental)
Interop and the operator kit, both experimental: outside the 2.x compatibility promise, they may change or go away in any 2.x release, and the 2.0 default resolution (TAP-10 / TAP-11 §2) is unchanged. Experimental: visitor grants. A visitor can let site A call a few named methods of service B for a short time, and revoke it. B checks the grant; nothing in it moves money. This is a prototype in the SDK, not a TAP. Every cross-app call is an off-chain request, so the site is no longer "100% on-chain"; grants and revocations are enforced by the called service only and are useless against a malicious one; a receipt proves what was signed, not that the data is right, nor which upstream model ran; until shells check it, the visitor compares the caller named in the grant with the site the wallet names. The escrow that paid calls rely on is still not audited and not deployed, so nothing below that pays works on mainnet; the interop examples run on the SDK's fake chain and in tests only. TAP-11, TAP-13 and TAP-14 to TAP-18 are merged as Draft (merging is not adoption). The new guide page is Interop between apps and containers.
examples/container-interop/: three kinds of container-to-container interop with the existing API, with a command-line entry (run.mjs) and a gaps table in its README: (a) an agent calls another container's paid method inside the cap of a mandate (the escrow is not audited and not deployed: not usable on mainnet); (b) a task thread from container A to container B, with A's own acceptance check; (c) two containers in a private channel (TAPI-26 v2 labels, the default since 2.0, passed explicitly) and a signed receipt for a request. A receipt proves only that the container signed that answer to that request. Fake chain only, on the 1.x resolution path (its own client saysdev: true; the clients it takes from the agent-service demo world passconform: false).examples/app-interop/(a local demo): one app calls another through TapeAPI. App A finds app B by name, reads its manifest, calls a free method with a visitor grant and a priced one with a voucher, and checks every answer. Fake chain only; the escrow there is a model (the contract is not audited and not deployed). Its clients read the fake chain the 1.x way and say so withdev: true: the 2.0 default refuses the fake chain's own hub and factory, so the conformance mode is not exercised.examples/app-interop/wallet-render/is a local, static page for checking how a wallet displays the grant before signing (eth_requestAccounts,eth_chainIdandeth_signTypedData_v4only; no transaction, no network request), with an offline checker of the copied result.- Visitor grants between apps: new package
@tapeapi/interop(experimental, a prototype), next to the SDK. Not a specification and not a TAP; no service knows the format, and the whole package is outside the compatibility promise. A visitor's wallet signs aVisitorGrant(its own EIP-712 domain "TapeAPI Visitor Grant", never TAP-11's) that lets one app call a few methods of another app's service for a short time (1 hour by default, 7 days at most), a bounded number of times, with no payment; the first field is a statement generated from the other fields. The calling app adds the top-level request membergrantwith a call proof signed by its one-off key (withGrant); the called service checks it (verifyVisitorGrant,grantMethods), counts it, takes revocations (grantRevokeHandler, free methodgrant_revoke) and writesresult.grantinto its signed answer; refusals areBAD_REQUESTwith the message prefixgrant: <reason>and noerror.data. Self-test vectorsspec/vectors/interop-visitor-grant.json, reproduced byverify.py. Boundaries: a cross-app call is an off-chain request; revocation and the count are enforced by the called service only; the reference storememoryGrantStoreis in memory, one process, and loses revocations, counts and replay records on restart (a production service persists them); its revocations are bounded (maxRevocations, 100 000 by default), dropped once their grant has expired, and a full store refuses a new one. An answer lost after delivery is counted once: when the SDK retries the same id (after a transport failure, on the next endpoint), the service sends the envelope it signed the first time again, byte for byte (not signed or counted again), if it still keeps it (bound to the grant, the id and the request, for at most 300 seconds after itsts;memoryGrantStore'smaxAnswers, 1 000 by default, andmaxAnswerBytes, 16 MiB); otherwise, as before,grant: replayed. Until shells check it, the visitor compares the caller named in the grant with the site the wallet names; how wallets display it was not tested. The grant check's chain reads (chainReadsFromApi) follow the 2.0 default resolution:siteStatusat a TAP-10 pinned block, with opening and activation checked and a cache of at most 60 seconds, so a name the defaultresolverefuses is refused by the grant check too (grant: caller-not-opened,grant: caller-unpaid, andaudience-not-opened,audience-unpaidfor the called service itself). The check reads the called service's holder and the caller's name only after its cheap steps and the visitor's signature, so a grant that fails one of them costs no chain read.grantMethodsgives a call with a grant its own deadline (timeoutMs, 20 seconds by default,GRANT_HANDLER_TIMEOUT_MS; keep it below the provider'shandlerTimeoutMs): past it the call is answeredINTERNALand not counted, even if the handler finishes later. An optional rate-limit window per (caller, visitor), keyed on what the check proved and shared by all their grants, lives in the grant store (grantMethods'perGrantWindow: { calls, windowS }, off by default;memoryGrantStore'stake); one more call is refusedgrant: rate-limitedand not counted. The server's limiter is unchanged: a call with a grant still spends the IP's free budget, and bad grants are not counted in its failure budget. @tapeapi/server: handlers get a read-onlyctx.request(experimental): a frozen JSON copy of the request's top-level members other thanmethodandparams(itsid, avoucherif sent, and any member TAP-13 does not define, such asgrant). Nothing the provider signs or bills changes. Two more experimental members serve a retry of the same id:ctx.onSigned(fn)is told the envelope of this call once it has passed every check and is about to be sent, and a handler may returnctx.resend(env)to send again an ok envelope this provider signed earlier over this very request (same id, method and params; checked), on a free call only.examples/new-api-sidecar: the operator's kit (experimental;OPERATOR-KIT.md, both languages). For a relay that already runs new-api:docker-compose.sidecar.yml,tapeapi-sidecar.env.example(placeholders only) andadd-sidecar.shadd the sidecar to the existing compose under a hostname of its own (the API hostname stays as it is; the script reads only the address in/tapeapi/v1/health, never a key). Two new optional variables, in the sidecar (server.mjs) and in the Worker (examples/ai-proxy/worker.js, withwrangler.toml):OPERATOR_SITE_URL, whereGET /andHEAD /on the sidecar's host answer 302 (https, no credentials, query or fragment, not tapeapi.fun, not the sidecar itself; anything else keeps the sidecar in setup mode, naming the variable); andSIGNER_KEY_GENERATE=1withSIGNER_KEY_FILE, which makes the signing key on the operator's own server when the file does not exist yet (mode 0600, created exclusively, never overwritten; only the address is printed; not whenSIGNER_ADDRESSis set). The holder console's first delegation still signs only for the key its step 3 makes, so a key made on the server cannot be delegated from the console today;OPERATOR-KIT.mdsays so. A daily check for the operator's own GitHub repository,templates/tapeapi-daily-check.ymlwithtemplates/daily-check.mjs: once a day,tapeapi-doctorby name with the 2.0 default resolution and the price converter's--check; the activation's end and the fee come from the doctor's structured--jsonfields (paidUntilSeconds,monthlyFeeWei); 30 days or fewer left, or amonthlyFee()that is no longer 0, is a warning, and a failed check, no activation, 14 days or fewer, or a price drift needs action; an alert is an issue in that repository, nothing else. It uses no secret and pins every action to a commit;TAPEAPI_COMMITis the commit of this repository the operator has read. Nothing in the kit names an operator, and the rules are the same for every service.- reconcile and new-api, end to end on a mock.
examples/new-api-sidecar/fake-new-api.mjsalso serves the usage log in the shapetapeapi reconcile --preset new-apiexpects (GET /api/log/self:data.itemswithupstream_request_id,model_name,prompt_tokens,completion_tokens,quota,created_at;GET /api/log/tokenfor a relay key, a baredataarray), one row per answered call,quotafrom its own ratios; the newreconcile-new-api.test.mjsruns the sidecar (FORMATS=openai-chat),tapeapi-verify --log, the pulled export andtapeapi reconcile --preset new-api --amount-scale <quota_per_unit>through the real binary: every row matched byupstream_request_idwith amounts compared (exit 0), then a row without that id joined by time, model and tokens, a differing row, a receipt with no row and a row with no receipt (exit 1), and the dashboard CSV's headers refused (exit 2). The reconcile guide gains a "With new-api" section with the export commands and the caveat that the dashboard's CSV export is another file. The field names are still read from new-api's source, not checked against a real instance, and the mock is not new-api.
Added: a client-side budget and a spend statement (experimental, 3.0)
- A client-side budget from your own policy file (experimental, 3.0):
createVerifyingFetch({ policy, budgetStore }),ai.createBudget,ai.budgetFileStore,ai.memoryBudgetStore. The same{ "tapeapiPolicy": 1, rules }file thattapeapi reconcile --policyreads now also refuses a call before it is sent when its worst case (ai.maxAmountOfover the request's declared output limit and its size in bytes) would take a covering rule over its limit:BUDGET_EXCEEDED(the code WebMCP's spending budget already used; heredata: { reason: 'budget-cap', rule, period, window, limit, currency?, cap, spent, worstCase }), orINVALID_ARGUMENTwithdata.reasonwhen the call cannot be bounded (no output limit, no price-table entry, no price in the rule's currency, a body that is not JSON, a store that cannot be written). The store is an append-only JSON-lines file (mode 600) of verified receipt envelopes and one "sent" line per call, so the count survives a restart and a call with no verified receipt counts at its worst case, unless it ended in a definite failure that claims nothing (an HTTP 4xx or 5xx with no receipt): then a "released" line drops it (a timeout or a broken connection keeps counting).tapeapi receiptsandtapeapi reconcileread the store as a receipt file (sent and released lines are skipped, not listed as skipped). The agent kit's limiter takes the same budget:createSpendLimiter().payer({ budget, tokenDecimals })admits each voucher's price before it is handed out, and a voucher released before it is accepted gets an offsetting released line. tapeapi-verify --policy <file> [--budget-store <file>](experimental, 3.0): the same budget in the local proxy (Claude Code, Codex, OpenAI SDKs through it). A refused call gets HTTP 400 withx-should-retry: false(codebudget_exceeded, orbudget_no_limit,budget_no_price, ... when it cannot be bounded), as the proxy's other refusals before forwarding, and is not forwarded; its--logline carriesbudget. Off unless given. A convenience in your own process, not an enforcement anyone else can rely on; it bounds the prices the receipts claim, not what anyone pays, and needs no escrow. Off unless given: defaults are unchanged.tapeapi receipts statement --receipts <file|dir> --from <t> --to <t> [--policy] [--mandate](experimental, 3.0): a spend statement per service, model and UTC day, JSON or text (en, zh), with the policy's and the mandate's caps alongside, counted asreconcilecounts (its totals equal reconcile's on the same files). Signed by nothing: it is the agent's or buyer's own summary. The agent kit'sspendStatement/spendEvidence(phase 1) are not reused: they state an escrow channel'sclaimedOfat delivery and need an escrow.- The policy-file code (
policyOf,policyCheck,claimOf, the amount helpers) moved fromreconcile-checks.jstosdk/src/policy.jsso the fetch can use it without an import cycle;reconcile-checks.jsre-exports every name.tapeapi reconcile's policy note now reads "reconcile enforces nothing" instead of "it was not enforced when the calls were made" (a client-side budget may have read the same file).
Changed: bounded agents before any audit (experimental, phase 1; the escrow is not audited and not deployed, so none of this works on mainnet)
- Spend limiter: one call is bounded by the method's listed price too: the per-call ceiling is the lower of
maxCallPrice(or 10% of the cap) and the method'spriceBEM(spendDecision({ listedPrice });api.callpassesextra.methodto the payer), and a method listed free is never paid. With the client-side budget above (payer({ budget, tokenDecimals })), the two compose as an AND in a fixed order: the limiter's own rules first (the per-call ceiling, the cap overclaimedAtCheck, the stops; pure, in the token's smallest unit), then the budget's rules (the voucher's price in the token's label, per covering rule of the policy file). A limiter refusal writes no budget line; a budget refusal, or any later failure before the voucher leaves, releases the lease and offsets the line; the listed price never changes what the budget counts (the voucher's price, not the method's). - Close-out in explicit, resumable steps (BEM drill F10):
closeOutPlanaddssendNow/sendLater(a withdraw before its window reverts and wastes gas); newcloseOutNextandkit.closeOutStephand out requestWithdraw, then nothing through the 48-hour cooldown, then withdraw, each step once, from the chain at a pinned block and a JSON state the console keeps, so a restart in the window resumes. - Buyer's scan before funding (session keys survive a circuit's transfer): new
kit.inheritedSessionsreads theSessionAuthorizedlogs of the mandate's channels from a block at least 30 days old and reports the live sessions the console's history does not record (on the container path, a previous holder's);holderPlan({ sessionScan })refuses on them (inherited-session-live), on logs it cannot read (session-logs-unavailable) and on a scan that starts too late (session-scan-short), or withrefuse: falsereturns the facts. It makes the exposure visible; it cannot end a session. The scan needsdeployBlocks(each escrow's deployment block): a node whose log window does not reach back answers an empty list rather than an error, so the escrow's own constructor log there must be served, or the scan issession-logs-unavailable;fromBlockmust be strictly older than 30 days, and a session whose expiry is the current second counts as live, assettlereads it. - Provider start-up checks the escrow's token:
listen()reads the escrow'stoken()and refuses to start when it is not the token the manifest prices in (BEM on BNB Smart Chain, orcreateProvider({ escrowToken });UNSUPPORTED_PAYMENT_TOKEN, reasonescrow-token-mismatch; withdeva log line); an escrow withouttoken()or nodes that cannot answer are logged, never taken for a match; Workers callcheckEscrowToken(). - 3.0: the escrow token check guards every paid call, on Workers too:
handleRequest()andinvoke()run it before the first paid call, so a Worker no longer awaitscheckEscrowToken()itself, and an escrow withouttoken()or nodes that cannot read it now refuse paid calls as a mismatch does: an unsigned HTTP 503UNSUPPORTED_PAYMENT_TOKEN(data.reasonescrow-token-mismatch,escrow-no-tokenorescrow-token-unreadable), nothing signed or billed; a refused call counts against the caller's free budget, as unverified traffic does. A definite answer (match, mismatch, skipped) is kept for the instance, andcheckEscrowToken()called directly returns it instead of reading again (review R30B-2); a failed read is tried again at the next paid call and never kept as a match. The check is skipped, and paid calls go on without it, with no priced method, no escrow,settlementFactsorenforcementconfigured, norpcUrls, or a chain other than BNB Smart Chain withoutescrowToken. Free methods,listen()refusing to start on a mismatch, anddevare unchanged;stats().escrowTokenRefusedis new.api.tapeapi.funandrelay.tapeapi.funserve free methods only and configure no escrow, so they are not affected. - The unsigned 503
UNSUPPORTED_PAYMENT_TOKENis documented (docs only): TAPI-22 §3.2 (en, zh; an informative note, no rule changed; TAPI-21 is untouched) and the provide and consume guides say what its three reasons mean to a caller:escrow-token-mismatchis kept for the provider instance (do not retry), whileescrow-no-tokenandescrow-token-unreadableare read again at the next paid call. The SDK reports the answer asPROVIDER_UNAVAILABLE, as before.
Fixed: hardening from internal reviews (experimental features above)
- The client-side budget counts more conservatively (experimental, 3.0, internal review BR-1..BR-9). A receipt that answers a call now counts under the names the request went by as well as the reported model (an alias or a dated snapshot no longer escapes a model rule), in the window of the call's own time (not the signer's clock), and at the call's worst case where it claims nothing in the rule's currency (no usage, or a reported model the price table lacks); each receipt answers one call. A model the price table does not know is covered by every model rule of its service. Refused as unbounded (
budget-no-limit): an output limit of 0, a Chatnthat is not a whole number of at least 1, and a Responses call withprevious_response_idorconversation. A store whose last line was cut short no longer swallows the next line; a receipt envelope carrying extra top-level keys is always read as a receipt; budgets on one store file read each other's new lines before every admit, under a lock file<store>.lock(flag wx, stale after 30 s) whenbudgetFileStoreis givenwriteFileSyncandunlinkSync, astapeapi-verify --budget-storedoes. A call to a model the price table lacks names that model and the rule's model in its refusal.tapeapi-verify --save-requestsno longer saves a call the budget refused.tapeapi reconcile: a time with no offset (2026-10-22 10:00:00) is read as UTC, in the bill and in--since/--untilalike (it was the machine's zone);--mapnaming a key twice or a column the header has twice is a usage mistake, and a row whose id belongs to a receipt already joined (the same id billed twice) stays bill-only instead of being joined by time. - Visitor grants hardened (experimental, internal review IR-1..IR-6, IR-12). The check order is now call proof, replay (a resend of a call still in flight is refused there too, before any read or the per-grant window), the visitor's signature, then revocation, so a forged grant cannot probe a visitor's revocations, and a revoked grant is never answered with a kept envelope; a positive EIP-1271 answer is cached for
cacheS.memoryGrantStorekeeps revocations of grant hashes it never saw within a budget of their own (maxUnseenRevocations, half ofmaxRevocationsby default), so throwaway keys cannot crowd out the revocations of grants it has seen. A call id that is not well-formed Unicode is refused.chainReadsFromApicacheseth_getCodeper address and bounds its cache (maxCacheEntries).grantManifestProblems(manifest, { methods })names a grant method whose handlergrantMethodsdid not wrap. spec/TAPI-28.md (en, zh) states the new order and the limits that remain (a revocation names no service; holder checks are point reads; a forged signature can cost one EIP-1271 read). New refusal vectorsspec/vectors/interop-visitor-grant-negative.json: 48 requests refused at TAPI-28 steps 1 to 8, each with its reason, and control cases, rebuilt byte for byte by the SDK tests and recomputed independently byverify.py;interop-visitor-grant.jsonis unchanged. Still a prototype, not a TAP. - Close-out at the finalized block, by the chain's time, and replacing a stuck step:
closeOut/closeOutStepread their facts at the block the nodes call finalized, and a step is due only once that block's time says so (a clock ahead of the chain no longer hands out a withdraw that reverts);inheritedSessionsjudges liveness by the earlier of the clock and the pinned block's time. NewcloseOutSent(state, { provider, nonce })records the nonce a step was sent with; a step handed out again then carriesreplaceNonce, to be sent with that nonce so it replaces the stuck transaction instead of a secondrequestWithdrawrestarting the 48 hours. - Close-out state from the future: a kept state whose
issuedAtis afternow(a clock set back, a file edited by hand) no longer holds a step back for good; the step is handed out again and logged (state-from-future). - Tests (no change to shipped code): property tests with a seeded PRNG and no new dependency (receipts, prices,
toolsSha256, reconcile, the voucher digest, grants); three timing-dependent tests made deterministic with an injected clock instead of retried; the budget's UTC day windows and the MCP caller tag's day no longer depend on when a run happens; every child process a test file starts itself is bounded (300 s, then SIGKILL; six that already had a shorter bound keep it), while the children that an example script starts when a test runs it are not. - Release check (no change to shipped code): the SDK tarball's unpacked-size guard in
scripts/packages.test.mjsis 3 MiB (it was 2.5 MiB; the SDK unpacks to 2,593,875 bytes). It catches a file packed by mistake; it is not a budget.
Experimental: x402 batch-settlement enforcement backend
- Prototype, experimental:
{ enforcement: 'x402-batch' }, x402 batch-settlement channels as the enforcement behind the phase-1 checks, the spend limiter and the budget. The enforcement is now a pluggable backend (enforcementBackend,ENFORCEMENTS;createProvider({ enforcement })on the provider side);escrow-channelmoved behind it with no change in behaviour. The new backend uses contracts already deployed on BNB Smart Chain that we did not write and have not reviewed (x402BatchSettlement0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003,Permit2DepositCollector, Permit2; their repository carries external review reports we have not assessed):verifyMandate(signed, { enforcement: 'x402-batch', x402: { terms } })reads the channel at one pinned block,x402Payersigns the agent key's vouchers and stops at itssessionExpiry(default one hour: the contract has no expiry),x402Acceptancechecks them for a provider and gives it its ownclaimandsettle,x402Txbuilds every unsigned transaction. EOA payers only (a TapeOut container'sisValidSignatureanswers 0:x402-payer-not-eoa); no contribution, no treasury; the service sets the withdraw delay and the client refuses one above its cap (48 hours by default); what a receipt proves is unchanged. The SDK sends nothing and pays no gas for anyone. Tested on a fake contract and once on a local fork; never on mainnet. See Container agents.
Spec notes and receipt references (proposal)
Two kinds of change. The first three entries are editorial: specification and documentation wording only; no rule, field, digest or vector changed. The last three are a proposal, not normative: the two helpers and the sidecar option are experimental and change nothing unless called or set, and none of the three is part of TAPI-21 or of any TAP.
- TAPI-20 §3.8, two informative notes:
toolsSha256covers exactlyname,title,description,inputSchema,outputSchemaandannotationsof each tool, and nothing else (noticons, not_meta, nothing outside the tools; pages merged first); the reference MCP proxy andtapeapi-mcppass on only those members. A server whosetools/listdepends on the caller's authorization (allowed by MCP 2026-07-28) cannot be pinned by one whole-list digest: callers that see another list are refused (tapeapi-mcprefuses the calls; the proxy answersTOOLS_CHANGED). Recorded in theRevisionrow. - What a receipt's answer hash covers: TAPI-21 §1 and §8 (with a new
Revisionrow), the READMEs, the guides (AI providers, billing layer, reconcile, receipts and payment protocols, en and zh-CN), the AI sidecar example, the verify page and the notes oftapeapi connect,tapeapi receiptsand the relay trial no longer say "exactly which response bytes": a whole answer is hashed as its exact bytes, a stream as its event data read by the §3.5 rule (not its comments, event names, line ends or chunking). - Receipts and payment protocols: dated again: the x402 and AP2 files were rechecked on 2026-10-27 (read-only) and each statement says which reading it comes from: x402's v2 specification changed on 2026-10-26 (an optional
errorMessage; still no request or response hash), the batch-settlement directory now also holds EVM, SVM and Cloudflare network bindings (not summarised), AP2's latest tag is stillv0.2.0. ai.receiptRef(envelope)(@experimental (3.0)): one stable reference per signed usage receipt, the SHA-256 of the canonical JSON of its eight members (containerandsigin lower case,vas 27/28, other top-level members left out), so the header form, the SSE comment and thereceiptmethod's copy have the same name, for other records (a payment record, a review, an on-chain job) to cite. It identifies a receipt and proves nothing until the receipt is verified.ai.priceRefOf(field)(@experimental (3.0)) andcreateAIProxy({ priceRef: true })(off by default): the SHA-256 of the canonical JSON of the manifest'sai.models, reduced to the members TAPI-20 §3.9 defines, signed into every receipt asresult.priceRefwhen the option is on, so a verifier can tell which price table priced a receipt.verifyUsageReceiptreports a malformedpriceRefas a shape problem and a different basis as a warning; a different basis never excuses an amount that does not match. Off (the default), receipts are byte for byte as before; turning it on changes the signed content of every receipt, which the reference verifiers of 3.0 accept and ignore, while the submitted AI-receipt TAP draft does not allow a sidecar to add a result member. Neither addition is part of TAPI-21 or of any TAP.- Two new vector files, made by the real code (
sdk/test/receipt-refs-vectors.test.mjs) and checked independently byspec/vectors/verify.py:spec/vectors/tapi-21-price-ref.json(five price tables, four receipts signed with and without the option, a receipt checked against another table) andspec/vectors/tapi-21-receipt-ref.json(the references of eleven receipts, re-serialisations that keep one, changes that do not). No existing vector file changed.
Added: docs (experimental documents)
- Guide: Interop between apps and containers (en, zh-CN; experimental), with the boundary paragraph above. Not on the home page, the directory or the playground.
- Receipts and payment protocols (en, zh-CN): where a TapeAPI usage receipt sits next to x402, AP2, ERC-8004, ERC-8183 and IETF attestation-receipt drafts, and how a bounded-agent mandate lines up with an AP2 Payment Mandate (direction only: no adapter, no payment bridge); primary sources dated, unverified points marked.
- Circuit Jobs (en, zh-CN; 3.0, documentation only): what the draft TAPs #85 (submitted, now a draft PR) describes (an ERC-8183 job settled by the #74 dispute game), why TapeAPI ships no code for it (its Solidity reference is not audited and not deployed, nothing deployed, an upgradeable kernel, no demand evidence), and how it relates to receipts, the passport and policy circuits. Also: one table of the delegation and binding windows of TAP-18 (#47), #84, #83 W-1, TAP-11 and what
tapeapi initand the console propose today (Agent Passport), and one sentence in the receipts guide on the experimental x402 batch-settlement backend. - Release candidates (release rule): a 3.x RC is not published as a GitHub release unless the server and every 0.x package first take the SDK by a range that accepts the pre-release (such as
^3.0.0-0);^3.0.0does not, so npm would look for@tapeapi/sdkon the registry.scripts/packages.test.mjsfails when the SDK is a pre-release and a range does not match it. - TAP status brought up to date (2026-11-06). Seven of our drafts are merged as Draft (TAP-11, TAP-13, and, from 2026-11-03, TAP-14 to TAP-18; merging is not adoption); seven are submitted (in review; #60 is now a draft PR). The README, the roadmap, the guides and the code comments name the numbered drafts by number, with the pull request next to it. Text only; no behaviour changed.
[2.5.0] — 2026-11-06
TapeAPI 2.5: accountable first — the numbers line up. Experimental additions for the people who hold the ledger, on either side of a receipt. An operator's billing layer can charge from the receipt with the rule every receipt is signed with (ai.amountOf, ai.pricesOf, public again) and size a hold before the call (ai.maxAmountOf, ai.maxPricesOf, ai.completionLimitOf); the new-api and LiteLLM sidecars take an allowlist of upstream response headers (UPSTREAM_RESPONSE_HEADERS) and a receipt lookup limit the operator sets (RECEIPT_LOOKUPS_PER_MIN); a guide describes a billing layer in front of the sidecar. A buyer's tapeapi reconcile sets receipts against the requests that were sent (--requests), a mandate's caps (--mandate) and the buyer's own limits (--policy), from local files only, and lists each fact with the receipt line it comes from; --fail-on makes named kinds exit status 4 for a script, and tapeapi-verify --save-requests keeps the salted request bytes for that join (off by default: the files hold the prompts in clear). MCP clients of revision 2026-07-28 are served without initialize (server/discover, header checks), clients of earlier revisions get byte for byte the answers of 2.4.0, and createMcpProxy and tapeapi-mcp can be asked (upstreamNegotiation: 'auto', --upstream-negotiation auto) to read an upstream that speaks only 2026-07-28. All of this is experimental: outside the 2.x compatibility promise, off unless called or set, and nothing in it makes or takes a payment. Three things change without being asked for (see Changed): a value in the MCP _meta protocol-version key that is not a revision is refused, a request sent with MCP-Protocol-Version: 2026-07-28 but without the 2026-07-28 _meta is refused, and tapeapi reconcile --json carries three more keys. Pushing this release redeploys the public service, the relay and the website as usual; the public service's /mcp then answers server/discover and serves 2026-07-28 clients, and nothing else about the live services changes but the version number they report: no variable is set or needed, and the REST methods, the manifest and the receipts are as in 2.4.0. A receipt still shows what its signer said, not which model ran; these checks list facts, not verdicts.
Added: helpers for a billing layer in front of the AI sidecar (experimental)
All of these are experimental (outside the 2.x compatibility promise) and change nothing unless called or set: a sidecar that sets none of the new variables answers, signs and limits exactly as in 2.4.0. Nothing here makes or takes a payment, and a receipt still shows what its signer says, not which model ran.
ai.amountOfandai.pricesOfare public again, with a worst-case hold:ai.maxAmountOf,ai.maxPricesOf,ai.completionLimitOf(in@tapeapi/sdk/aiand the rootainamespace,@experimental (2.5)intypes/ai.d.ts).amountOf(price, usage)is the rule every receipt is signed with (per 1M tokens over disjoint buckets, rounded up once to 8 decimals); 1.0 had stopped exporting it (RC-7), and an operator's billing layer had to copy it by hand.maxAmountOf(price, { promptTokens, completionTokens })is the most one call can cost within those counts: every prompt token at the dearest ofinput,cacheRead,cacheWrite,cacheWrite1h(a cache write can cost more than input), every completion token at the dearer ofoutputandreasoning, rounded up once, so never belowamountOffor a usage within the counts;maxPricesOf(entry, bounds)gives it per currency.completionLimitOf(body, format)reads the completion limit a request declares (Chat Completions: the larger ofmax_completion_tokensandmax_tokens, timesn; Responses:max_output_tokens; Messages:max_tokens; Embeddings: 0), or null. Limits, stated in the declarations and the guide: a model that ignoresmax_tokens, a hidden system prompt, images or tool definitions can exceed any bound computed from the price table; settle on the receipt and keep a hard cap. Tests: the publicamountOfandpricesOfreproduce every amount ofsdk/test/fixtures/ai-receipt-vectors.json, andmaxAmountOfis never belowamountOfover a grid of usages within the bounds. The SDK grows by these five functions only; the playground's vendored copy of the SDK is rebuilt.createAIProxy({ upstreamResponseHeaders })andUPSTREAM_RESPONSE_HEADERSin the new-api and LiteLLM sidecars: an allowlist of the upstream response headers passed on to callers, so headers that name an operator's channels or costs need no second proxy. Set, only the listed upstream headers (case-insensitive) andcontent-type(always kept, as its media type withcharsetandboundaryonly: other parameters could carry what the list hides) andretry-after(always kept, so clients can back off) pass, on every/v1/*answer; the sidecar's own headers (the receipt, the sidecar-error mark, CORS) are added as before. Headers are not part of a receipt, so receipts and answer bytes are the same with or without it (tested on a whole answer and a stream). Unset or empty (the default), every upstream header passes as before. It filters headers only: the answer body, including itsidandmodel, passes unchanged.RECEIPT_LOOKUPS_PER_MINin the new-api and LiteLLM sidecars: thereceiptmethod's lookups per client IP per minute, as the Cloudflare Worker (examples/ai-proxy/worker.js) already read it. Unset or empty: 10, as before;0refuses every lookup, as in the Worker;offgives the method no budget of its own (RATE_IPonly); anything else is setup mode naming the variable, in the Worker too (it used to fail insidecreateAIProxy). 2.4 did not read this variable orUPSTREAM_RESPONSE_HEADERS: a malformed value left over from then is now setup mode. A billing layer that looks receipts up from one address shares one bucket and can now raise it without changing code.env.exampleanddocker-compose.ymlof both packages list both variables, commented out or empty.- Guide: "A billing layer in front of the sidecar" (
docs/guides/billing-layer.md,zh-CN/billing-layer.md): the topology (billing layer, sidecar, upstream; requests and answers byte for byte, the receipt passed on), whyFORMATSmust match the formats the billing layer meters and the price table'sformats, charging from the receipt withai.amountOf, sizing the hold withai.maxPricesOfand what it cannot bound (models that ignoremax_tokens, hidden prompts, images, the reported model, concurrency), receipt lookups, upstream headers, disclosing any model-name rewriting and logging both the requested and the upstream name, suggested wording ("Receipts in TapeAPI format, signed by <operator>; TapeAPI does not operate or endorse this service") and never claiming that a receipt proves the upstream model; and a table for upgrading a sidecar from 2.0 to 2.4 (FORMATSand the price converter in 2.1, receipts and reconcile in 2.2,initandpricesin 2.3, nothing for the AI sidecar in 2.4).
Added: MCP revision 2026-07-28 (experimental)
- MCP revision 2026-07-28 for clients that send no
initialize(experimental: outside the 2.x compatibility promise, it may change or go away in any 2.x release).createMcpServerin@tapeapi/sdk/mcp, and socreateMcpEndpoint,createMcpProxy's/mcp,tapeapi-mcpand the public Worker's/mcpand/mcp/pinned, serve a request whoseparams._meta['io.modelcontextprotocol/protocolVersion']is2026-07-28(with aclientCapabilitiesobject) statelessly: the result carriesresultType: "complete"and_meta['io.modelcontextprotocol/serverInfo'](placed beside a receipt, which keeps its place), list resultsttlMs: 0/cacheScope: "public";server/discoveranswerssupportedVersions(2026-07-28,2025-11-25,2025-06-18,2025-03-26),capabilities,instructionsand the cache hints; another version is refused with-32022{ supported, requested }, a version that is not a string or a missingclientCapabilitieswith-32602. On HTTP (createMcpEndpoint,createMcpProxy) a single such request is also checked against itsMCP-Protocol-Version,Mcp-MethodandMcp-Nameheaders (=?base64?...?=decoded): a missing or different value is-32020with HTTP 400; a refused version or malformed_metais HTTP 400; an unknown method is HTTP 404. These checks cannot be skipped (review 2026-10-22): a JSON array body (a batch) that holds a 2026-07-28 message, or that comes withMCP-Protocol-Version: 2026-07-28, is refused with HTTP 400 and-32600(id: null), since a gateway routing onMcp-Namecould otherwise be passed a header naming one tool while the batched body called another (Streamable HTTP "Sending Messages": the body is one message; "Server Validation") (review MR-1); a single request sent withMCP-Protocol-Version: 2026-07-28but no 2026-07-28_metais refused with 400 and-32020when itsMcp-MethodorMcp-Nameheader, or a_metaversion it carries, does not match the body (review MR-2). Only the exact header value2026-07-28counts: any other value is answered as 2.4.0 answered it. New SDK exports:requestEra,MCP_MODERN_PROTOCOL_VERSIONS,MCP_SUPPORTED_PROTOCOL_VERSIONS,MCP_ERRORS,PROTOCOL_VERSION_META,CLIENT_CAPABILITIES_META,SERVER_INFO_META. Requests of revisions 2025-11-25, 2025-06-18 and 2025-03-26 get byte for byte the answers of 2.4.0:server/test/fixtures/mcp-legacy-2.4.0.jsonholds 290 exchanges recorded from the 2.4.0 tree and the tests replay them; 285 match byte for byte, and the other 5 are the one recorded request that no client of an earlier revision sends (the 2026-07-28 header without its_meta, on each of the five HTTP entry points; see Changed). A randomized comparison against the 2.4.0 code (9 000 exchanges, legacy-shaped requests only) found no difference. Nothing signed changes (receipts, envelopes, verify links,toolsSha256).MCP_PROTOCOL_VERSIONS(theinitializelist), and theinitializethe proxy andtapeapi-mcpsend to an upstream MCP server, are unchanged by default.createMcpProxytakesupstreamNegotiation: 'auto'(experimental; default'legacy', as before): it probes the upstream withserver/discoverand speaks 2026-07-28 to one that offers it, so an upstream that speaks only 2026-07-28 can be proxied; otherwise it falls back toinitialize.stats()gainsupstreamEra.tapeapi-mcptakes the same choice as--upstream-negotiation legacy|auto(experimental; defaultlegacy, as before) for reading a taped-out MCP server's tools from itsmcp.endpoint, with the same fallback rules; its calls go through the signed TapeAPI path either way, and thetoolsSha256it checks and pins is the same (tested against an endpoint that speaks only 2026-07-28, which the default cannot read). The official TypeScript client v2 (@modelcontextprotocol/client2.3.1) was run locally against these entry points in its legacy,autoand pinned-2026-07-28 modes and against 2025-06-18 and 2025-03-26. Kept as before, on purpose:pingis answered for a 2026-07-28 request; a batch of older-revision messages is answered message by message with HTTP 200, as before;Mcp-Param-*headers are not checked on any entry point, and the MCP guide now says so plainly (review MR-9). Checked on 2026-10-21 against https://modelcontextprotocol.io/specification/2026-07-28/ (basic/versioning, basic/index, server/discover, basic/transports/streamable-http, basic/transports/stdio, changelog). The MCP guide (English and Chinese) describes it. /mcp/pinnedholds itstools/listtoMCP_TOOLS_SHA256as it holds the calls (review MR-7b, experimental): while the tools hash to anything else,tools/listis refused as the signing proxy refuses it (-32603,data: { code: "TOOLS_CHANGED", published, current }, HTTP 200). New optioncreateMcpEndpoint({ pinnedToolsSha256 })(experimental); without it nothing changes.tapeapi-mcpthen reports that the tools could not be read, and still sends nothing. WithMCP_TOOLS_SHA256unset (as on the live service),/mcp/pinnedstays 404 as in 2.4.0.- Opt-in
Origincheck (review MR-8, experimental):allowedOriginsoncreateMcpEndpointandcreateMcpProxy,MCP_ALLOWED_ORIGINSon the public Worker,ALLOWED_ORIGINSinexamples/mcp-proxy/index.mjs. An unlistedOriginis 403 before anything runs; a listed one is echoed instead of*; noOrigin, answered as usual. Default unchanged (every origin, CORS*). The MCP guide says to set it for a proxy run locally in front of tools with side effects.
Added: reconcile request facts, mandate caps and local limits (experimental)
tapeapi reconcile: facts about each request, a mandate's caps and your own limits (experimental, outside the 2.x compatibility promise; files only: no network, no key, nothing uploaded, no telemetry). Every fact names the receipt line it comes from, and the request file or bill line it uses; none is a verdict, and none changes the exit status unless--fail-onasks (--jsoncarries the counts). A receipt proves only what its signer said; it does not prove which model ran, and these checks cannot tell which model ran. Guide: Reconcile your receipts.--requests <dir>: request bodies you saved, joined to the receipts by SHA-256 (requestSha256). Listed:prompt_tokensthat differ within one request class (same model beyond--tolerance-tokens; across models beyond--prompt-spread, default 16),prompt_tokensabove the request's size in bytes,completion_tokensabove the request's output limit (withreasoning_tokens; the limit isai.completionLimitOf's, the rule a billing layer sizes its hold with: for Chat the larger ofmax_completion_tokensandmax_tokens, timesn), and the model names the receipts carry for one requested model, by stream and non-stream. Only the exact bytes sent match:tapeapi-verifysalts requests unless--no-salt;tapeapi-verify --save-requests <dir>keeps the salted bytes (below).- Three optional
--mapkeys give the same facts from a bill:request,request_bytes,max_tokens. A receipt and its bill row whose model names differ only as aliases in the snapshot are listed (the join counts them as one model, so they are not a mismatch). --mandate <file>: per scope item, the receipts of its provider signed in[notBefore, expires), summed and set against the cap (--token-decimals,--mandate-currency,--near); other containers and receipts outside the period are listed apart. A phase-0 mandate names no funds and is not compared; the signature is not checked.--policy <file>: your own limits ({ "tapeapiPolicy": 1, "rules": [...] }: amount, calls, prompt and completion tokens per service, model and call, day, month or total). Nothing is enforced at call time; the windows whose receipts claim more are listed. An unknown key is a usage mistake.--billis optional when any of the three is given. No per-provider preset is added: an operator exports its ledger with columns--mapcan name (the guide lists them).--fail-on <kinds>(opt-in):findings,mandate-above-cap,policy, comma-separated. When the exit status would be 0 and a named kind is listed, it is 4; 1, 2 and 3 stay as they are, and without--fail-onnothing changes (no new JSON key either; with it,failOn: { kinds, counts, listed }). A kind that could never be listed is a usage mistake:policywithout--policy,mandate-above-capwithout--mandateand--token-decimals. For a script or CI step that should stop on a fact; the facts are still facts, not verdicts.
tapeapi-verify --save-requests <dir>(experimental, off by default: a privacy trade-off). It writes the exact bytes of every metered request it sends, salt included, to<dir>, one<sha256>.requestfile each, sotapeapi reconcile --requests <dir>joins salted receipts. These files hold the prompts, files and tool output in clear, which neither the receipt nor--logholds;--help, the start-up line and both guides say so. The directory is created 700, an existing one open to group or others is refused (exit 1), each file is 600 and written once; a request over 16 MiB (which reconcile skips) is not written; a failed write is logged and the call goes on. Free paths are not saved, and the--logformat is unchanged. Tested end to end: a salted JSON call and a stream through the real binary, their files the bytes that reached the sidecar (one compared byte for byte, both by hash), and reconcile joining both.
Changed
- MCP: a single request sent with
MCP-Protocol-Version: 2026-07-28but no 2026-07-28_metais refused with HTTP 400 (review MR-2b, 2026-10-22):-32602(the required_metafield is missing) when no header contradicts the body,-32020when one does. 2.4.0 answered it as an older-revision request; no client of an older revision sends that header value.initializekeeps the older handshake whatever the header says. On each of the five HTTP entry points this is the one recorded 2.4.0 exchange that is answered differently. - MCP: a value of
_meta['io.modelcontextprotocol/protocolVersion']that is not a revision is refused. From 2.2.0 to 2.4.0 such a request was answered:"2027-01-01"(a later date) as 2026-07-28,"latest","2026-7-28"and a number as an earlier revision. Now a string is-32022and a number-32602(on HTTP, status 400), as revision 2026-07-28 asks; no client of 2025-11-25 or earlier sends the key. A legacy revision in that key ("2025-11-25", ...) is still answered as before. tapeapi reconcileprints more without the new options. Every--jsonoutput gains the keyschecks(always),mandateandpolicy(null unless asked for) andnotes.findings; with a manifest snapshot, the text output gains a findings section when one price-table entry carries several model names in the receipts, or a receipt and its bill row differ only as aliases. The exit status is unchanged (0 / 1 / 2 / 3 as in 2.4.0): findings change it only when the new--fail-onasks (exit 4).- The playground's vendored SDK and the site's asset hashes are rebuilt for the changed
sdk/src/mcp.jsandsdk/src/ai-public.js(whose header comment now says which price helpers are public again, as experimental).
[2.4.0] — 2026-10-21
TapeAPI 2.4.0: MCP tool results that the public service's Worker can sign and declare. The public Worker's code (examples/public-api) has eight tool_* methods whose signed results are MCP tool results, and a second MCP endpoint, /mcp/pinned, whose only tools they are; createMcpEndpoint takes mcpResults to serve such methods; and the holder console accepts an mcp field that binds some of the methods, not all. All of this is experimental: outside the 2.x compatibility promise, it may change or go away in any 2.x release. The public service (api.tapeapi.fun), the relay (relay.tapeapi.fun) and the website are redeployed from this repository's main, so they run 2.4 once it is pushed: the console's rule changes then. The rest stays off: the tool methods and /mcp/pinned exist only while MCP_TOOLS_SHA256 is set on the Worker, which a deploy neither sets nor clears and which is not set on the public service, and without it a 2.4 Worker answers as a 2.3 one, apart from the version; 11.1013.tape declares them only once its holder publishes a new manifest on chain. Until then its manifest and tapeapi connect 11.1013.tape (not-ai) are as they were. The variable's meaning changes for anyone who set it under 2.3 (see Changed). In the SDK only the card tapeapi connect shows for an MCP tool service changes (see Changed); the rest of the SDK's code and the command line tools are unchanged apart from the version. Nothing below makes or takes a payment, and a receipt still shows what its signer says, not which model ran.
Added
- The public service's tool methods and
/mcp/pinned(experimental, outside the 2.x compatibility promise). The public Worker's code (examples/public-api) has eight more free, read-only methods,tool_block_number,tool_balance,tool_token_info,tool_token_balance,tool_nft_owner,tool_pair_price,tool_bnb_usdandtool_tape_name: each makes the same read as its REST twin and answers, signed, exactly an MCP tool result{ content: [{ type: 'text', text: <the answer as JSON> }], structuredContent: <the answer> }, which is what TAPI-20 §3.8 and TAPs #16 §4.2 require of a method bound to a tool; a bad argument is a signedBAD_REQUESTas before. They are the only tools of a second MCP endpoint,/mcp/pinned, and only while the variableMCP_TOOLS_SHA256is set (64 hex digits, taken lowercased, surrounding spaces ignored; any other value, such as one with0x, keeps the whole service in setup mode, and itsproblemnamesMCP_TOOLS_SHA256and the form it needs): the manifest then lists them after the eight REST methods and ends withmcp: { endpoint: <PUBLIC_URL>/mcp/pinned, toolsSha256 }, and only they are held to that value (a signedTOOLS_CHANGED, HTTP 409, if their tool text no longer matches, or withcurrentnull while their tool list cannot be built; the REST methods are not tools of that endpoint and keep answering).tapeapi-mcpthen verifies and shows their answers, and offers only them;tapeapi connectlists all 16 as the manifest's free methods, not as tools, since it does not read the endpoint (see Changed). With the variable set,/mcpand the REST methods give the same answers as without it (the same results, the same/mcptools), and without it the Worker is exactly what it was (tested end to end on a local fake chain). On the public service this stays off:api.tapeapi.funruns this code once 2.4 is pushed, but serves the tool methods and/mcp/pinnedonly onceMCP_TOOLS_SHA256is set on it by hand, and11.1013.tapedeclares them only once its holder publishes such a manifest on chain; until both, its manifest and answers are as before (apart from the version) andtapeapi connect 11.1013.tapesaysnot-ai. New in@tapeapi/server/mcp: thecreateMcpEndpointoptionmcpResults(default false): when the methods' signed results are MCP tool results, a result is passed on after the provenance line (imitations of that line labelled, ascreateMcpProxydoes) instead of as JSON text.
Changed
- The public Worker's
MCP_TOOLS_SHA256declares/mcp/pinned, no longer/mcp(experimental). In 2.3 the variable addedmcp: { endpoint: <PUBLIC_URL>/mcp, toolsSha256 }to the manifest and held the eight REST methods, the tools of/mcp, to it. From 2.4/mcpis never declared and never held: the variable adds the tool methods, declares/mcp/pinned, and must be the digest of/mcp/pinned's tools (mcpToolsSha256(mcpToolsManifestOf(env))fromexamples/public-api/worker.js). A value computed for/mcpunder 2.3 and kept across a redeploy now declares/mcp/pinnedwith the wrong digest: the tool methods answer a signedTOOLS_CHANGED(409) while the REST methods and/mcpkeep answering. The public service has never set the variable; without it nothing changes. - The holder console accepts an
mcpfield whose endpoint's tools are some of the methods, not all (site/console). It refused a manifest with any method that is not one of the tools atmcp.endpoint(the rule of the signing proxy, where every method is a tool); TAPs #16 §4.1 allows methods that are not bound. It now refuses only anmcpfield that binds no method, and before the wallet asks it says which methods are the tools and which are plain methods that MCP clients do not show to a model. The digest, hidden-text and shape checks are unchanged. tapeapi connectlists an MCP tool service's free methods as methods, not as tools (experimental). A manifest may bind only some of its methods (TAPs #16 §4.1; with the variable set, the public Worker's binds eight of sixteen), and which methods are tools onlytools/listat the endpoint says, which this command does not read. The card's row said "tools 16 (the manifest's free methods)"; it now says "methods 16 (the manifest's free methods; the tools are the ones tools/list names)", and the note under it says that the card does not say which or how many are tools.tapeapi-mcp, which reads the list, offers only the tools, as before.--jsonkeepsservice.tools(2.3) with the same list.
[2.3.0] — 2026-10-19
TapeAPI 2.3.0: from nothing to live, step by step, and fewer checks lost to a node that failed for a moment. tapeapi init walks an operator through the steps to a live AI service, one step per run, and checks each step read-only; tapeapi prices runs the new-api price converter and shows a price table with its source record; tapeapi connect also shows an MCP tool service that a manifest declares; and a chain read whose nodes failed fast and by the network is asked again, at most twice, before it is unavailable. The new commands, the connect card and the retry are experimental: outside the 2.x compatibility promise, they may change or go away in any 2.x release. One default changes: the retry is on in the SDK, in the commands and in createProvider (rpcRetries: 0 gives the previous behaviour request for request); a read answered in its first round is unchanged, and with the defaults the retries add less than 5.2 s to a read that fails. tapeapi --help is 12 lines longer, and tapeapi-mcp now refuses an mcp field that TAPI-20 §3.8 does not allow (capital hex digits, an endpoint with a query or fragment; no live manifest is known to carry one). Nothing below makes or takes a payment, and a receipt still shows what its signer says, not which model ran.
Added
tapeapi initandtapeapi prices convert|check|show(experimental: outside the 2.x compatibility promise, they may change or go away in any 2.x release).initis the operator's wizard from nothing to live, steps 0 to 7 (local trial, circuit, container and activation, the sidecar, the console's service key and delegation, publishing, users, the daily check and renewals), one step per run (--step,--renew): what the operator does, where and who pays (the operator; every paying step says we do not advance it), then the step's check, read-only, through the doctor'sdiagnose(),connectReport()and the price table files, and the next command. It asks for no key, password or token, never reads.env, uploads nothing, pays nothing and sends no transaction; its first screen says that a receipt proves only what its signer says. From step 3 on,FORMATShas no default: no--formats(or an empty value, or only--yes) is exit 2 naming it; nor has the gateway: without a terminal, steps 3 and 4 without--upstream new-api(or"upstream"in--answers, or a saved answer) are exit 2 naming it; all four formats need--confirm-all-formats(the output says they all go into the manifest on chain, whose history cannot be deleted); on a terminal each format is asked one by one, default No. The.envit writes once (mode 600; an existing one is never read) saysFORMATS=exactly the choice and leaves every secret empty; it is written again for a corrected--sidecar-url(or formats, or name) only while it is exactly as the wizard wrote it (its inode, size, mtime and ctime as recorded intapeapi-init.json, so nothing was filled in), and otherwise the wizard says to correctPUBLIC_URLby hand; steps 4 and 5 compare the formats the manifest declares with the choice.prices convertandprices checkrunexamples/new-api-sidecar/pricing-convert.mjswith the arguments as given (--fetched-as,--quota-per-unit, ...), its output and exit status unchanged, in a checkout only (outside one they print the clone command);prices showprints a price table with its source record (who fetched the price list, the rate,quota_per_unit, refusals and flags, what is not verified), files only. Exit status 0 / 1 / 2 / 3 as the doctor's (prices convert|check: the converter's 0 / 1 / 2);--json,--lang en|zh|both; output redacted astapeapi connect's. Files:sdk/src/init-cli.js,sdk/src/prices-cli.js(no new export subpath; theaiface is unchanged).tapeapi verify|mcp,connect,receiptsandreconcileare unchanged;tapeapi --helplists the two new commands.tapeapi-doctor: in setup mode themanifest-formatcheck carries what is missing as a field (experimental, like the doctor itself). When the sidecar answers in setup mode, the check in--json(and indiagnose()'s report) hassetup: { missing, problem, signer }, taken from the sidecar's/tapeapi/v1/healthas given (missingthe names it lists,problemthe configuration error it names ornull,signerits signing address ornull);tapeapi initreads it in steps 3 and 4 instead of the Englishdetail(which it still reads from a doctor without the field). Only that one key is added: the check's status, itsdetail, the text report and the exit codes are unchanged (setupFactsinsdk/src/doctor.js).tapeapi connectfor MCP tool services (experimental, outside the 2.x compatibility promise). A manifest with noaifield but a validmcpfield (TAPI-20 §3.8:endpointandtoolsSha256) is shown as usable: the endpoint, the number and names of its tools (the manifest's free methods, the onestapeapi-mcpcan offer: it offers no priced method), the first 8 hex digits oftoolsSha256, the signer, delegation and activation, and settings made from that field only: Claude Code and Claude Desktop throughtapeapi-mcp(stdio: every answer checked on the user's machine, the tools pinned) and the declared Streamable HTTP endpoint direct (marked as not checked). No setting carries a key. Nothing is fetched from the endpoint. Unavailable states and an invalidmcpfield (mcp-invalid) are exit 1 with the reason and no settings;--toolalso takesclaude-desktop;--proxyrunstapeapi-mcp(its JSON-RPC alone on stdout, the card on stderr);--jsongainskind: 'ai' | 'mcp' | null(added last, nothing else changes);@tapeapi/sdk/connectgainsmcpSnippetsFor,MCP_NOTESandMCP_SNIPPET_TOOLS. An AI service's card, settings and--json(apart fromkind) and every unavailable state's text are byte for byte as before.tapeapi initstep 6 does not take an MCP tool service for the AI service it sets up.- A read whose nodes failed only by the network, and fast, is asked again before it is
unavailable(experimental: client behaviour only, no TAP, wire format, status name or exit code changes; outside the 2.x compatibility promise).createRpcandcreateTapeAPItakerpcRetries(default 2, 0 to 5),rpcRetryMaxMs(default 2000, 0 to 10000) andrpcRetryFastMs(default 2000, 0 to 10000), also per chain inchains[id]; the commands built on them (tapeapi-doctor,tapeapi-verify,tapeapi-mcp,tapeapi connect) use the defaults. The server retries too:createProvider(@tapeapi/server) reads the chain throughcreateRpc(the block number aside, which is not retried: the escrow, channel and session reads of a paid call, the price feeds of USD pricing), and so does the examples' chain reader (examples/_lib/chain.mjs,createChainReader, which the public service's Worker uses for its methods); both now takerpcRetrieswith the same meaning, passed to every client they build (default the SDK's, 2;rpcRetries: 0turns it off). A read is sent again to every node, at mostrpcRetriesmore times, when a round of it gets too few answers, every node that failed did so with no answer and by the network (a broken or refused connection, a DNS failure, an HTTP 5xx without a JSON-RPC answer other than the gateway timeouts 504, 522 and 524), the round took less thanrpcRetryFastMsfrom its first request to its last failure, and the nodes that answered agree; the waits before the extra rounds are about 0.2 to 0.4 s and 0.4 to 0.8 s (exponential with jitter) and add up to at mostrpcRetryMaxMs. Each attempt needs the same agreement as the first: a strict read (TAP-10 §5.2) still needs every configured node, never two of three. Never retried: a timeout or an abort (a round in which any node timed out is not retried, so a node that keeps timing out costs a failing read one request timeout,rpcTimeoutMs, 8 s by default, as before), a gateway timeout (HTTP 504, 522, 524), a round that tookrpcRetryFastMsor longer (a node that resets just before its timeout costs a failing read what it cost before), a disagreement (the single re-ask after one, since 1.x, is unchanged and happens at most once per read), a JSON-RPC error or revert, a refusal (rate limit, method not found, HTTP 4xx), an answer that is too large or not JSON. When the last attempt fails, its error is the one a client without retries gives: the same code, message anddata, sounavailable,RPC_UNAVAILABLEand the doctor's exit code 3 read exactly as before. One difference in wording, not in outcome: when every node is down, the read a resolution names in its message can be another one than before (a read that is not retried, such as the pin'seth_blockNumber, fails first while the retried one waits); the status and the exit code are the same. Time: the retries add to a failing read at mostrpcRetriesxrpcRetryFastMsplus the waits, less than 5.2 s with the defaults, whateverrpcTimeoutMsis. The longest a read can take is therefore what it could take before plus 5.2 s: with the defaultrpcTimeoutMsof 8 s, 38.0 s instead of 32.8 s (a read that meets a disagreement, its re-ask and broken connections, each request ending just before its timeout), and in theory 54.0 s instead of 48.8 s for a batchedeth_call; with the 25 s of the monitor (scripts/monitor.mjs), 106.0 s instead of 100.8 s, and 156.0 s instead of 150.8 s. One read sends at most (2 +rpcRetries) rounds of one request per node, each re-asked at most once after a broken connection as before: 24 requests with three nodes.rpcRetries: 0(orrpcRetryMaxMs: 0, orrpcRetryFastMs: 0) gives the previous behaviour request for request.rpc.single()clients (the ChannelBus reader, proofs) do not retry; they poll again on their own. On 2026-10-15 one public BNB Chain node answeredfetch failedandtapeapi-doctorsaid undecided (exit 3); the cause of that failure was not recorded, so whether it was two failed connections in a row or a timeout cannot be told. 2.3 retries only failures of the network kind that come back fast: a broken or refused connection like that no longer stops a check, while a timeout (a connect timeout included) is still not retried and still reads undecided. @tapeapi/server/mcp:toolsSha256helpers and a signedTOOLS_CHANGEDhold forcreateMcpEndpoint(experimental, outside the 2.x compatibility promise; additions only).createMcpEndpoint()also returnstoolsSha256(), the SHA-256 of its owntools/listanswer exactly as clients compute it (the SDK'smcp.toolsDigest; the vectors of the MCP tools draft, TAPs #16, agree on the SDK, the holder console and the endpoint). New:listedMcpTools(manifest),mcpToolsSha256(manifest)andholdToToolsSha256(methods, { manifest, published }), which refuses every method that is a tool with a signedTOOLS_CHANGED(HTTP 409,error.dataexactly{ published, current }) while the tools no longer hash to the published value, and passes every other method through;publishedis the value the holder published, never one computed by the provider. A manifestmcpfield (TAPI-20 §3.8) made with them is conformant only when the provider's methods answer with MCP tool results ({ content, ... }): §3.8 and TAPs #16 are written for a signing proxy in front of a real MCP server (createMcpProxy, which already computes and holds itstoolsSha256), where each method is a tool, whilecreateMcpEndpointgenerates its tools from methods that answer with their own JSON, andtapeapi-mcpverifies such an answer and then does not show it as a tool result (tested). For that reason the public service11.1013.tapedoes not declaremcp: its manifest is unchanged andtapeapi connectkeeps showing it asnot-ai(see the MCP guide). The public Worker (examples/public-api) addsmcp: { endpoint: <PUBLIC_URL>/mcp, toolsSha256 }to its manifest and holds its methods only when the new variableMCP_TOOLS_SHA256is set (64 hex digits, taken lowercased, surrounding spaces ignored; any other value, such as one with0x, keeps the whole service in setup mode, and itsproblemnamesMCP_TOOLS_SHA256and the form it needs); the public service does not set it, and without it the manifest and every answer are byte for byte as before.
Changed
tapeapi connect: thenot-aireason names both fields. A manifest with neitherainormcpis still unavailable with statusnot-aiand exit 1; the reason now reads "its manifest has no ai field and no MCP tool declaration (mcp, TAPI-20 §3.8): it is neither an AI service nor an MCP tool service" (Chinese: "它的清单没有 ai 字段,也没有 MCP 工具声明……"). Scripts that matched the old sentence should match the status instead.
Fixed
tapeapi-mcp: a manifest'smcpfield is checked as TAPI-20 §3.8 requires (a correction to conform to the specification's MUST, in a stable command). §3.8 has required, sincetapeapi-mcpbegan checking the field in 0.4.0, thatmcp.toolsSha256be 64 lowercase hex digits and thatmcp.endpointhave no query or fragment;tapeapi-mcpaccepted capitals (and lowercased them) and did not look for a query or a fragment, which is looser than the specification. It now refuses such a field as unusable, before anything is sent to the service, with the reason "mcp.toolsSha256 must be 64 lowercase hex characters" (was "64 hex characters") or "mcp.endpoint must have no query or fragment". The MCP tools draft (TAPs #16) writes the same rules (§2) and lists both checks as planned for this implementation ("Reject upper case", "Add the missing checks"). Pin files already written are not affected:tapeapi-mcpstill pins the digest lowercased, so no pin hash changes. No publishedmcpmanifest is known to be refused by this: #16 records that no live service publishes anmcpmember, the holder console andcreateMcpProxywrite the digest in lower case, and the public service does not declaremcp(see Added).tapeapi connect, whose MCP card is new in this release, checks the field the same way from the start (mcp-invalid, exit 1).- The escrow deployment scripts (
scripts/preflight.mjs,scripts/verify-bscscan.mjs), found in a rehearsal on a local fork of BNB Smart Chain; they matter only to whoever deploys the escrow contract, which is not audited and not deployed.preflight.mjsread the escrow's token fromBEMwhilecontracts/script/Deploy.s.solreadsTOKEN, so an environment set up for the deploy script failed with "BEM is not set": it now reads--token/TOKEN, keeps--bem/BEMas a deprecated alias with a warning, fails when both are set and differ, and no longer echoes a value that is not an address. WithDEPLOY_ESCROW=trueit also checks the token (a new section 3b, read by quorum): it printskeccak256of the token's code and fails if a token it knows (BEM, or the oneDeploy.s.solpins) has other code, fails if any of the five proxy slotsDeploy.s.solreads is not zero, and fails unlessdecimals()is between 8 and 18.verify-bscscan.mjs --dry-runno longer asks Etherscan whether the address is verified (it did, before looking at--dry-run): a dry run builds, recompiles, compares with the code fromRPC_URLand stops;--checkis the way to ask.
[2.2.0] — 2026-10-19
TapeAPI 2.2.0: receipts put to use, against a bill and against the paid period. tapeapi receipts lists, replays and exports the receipts tapeapi-verify --log saved, tapeapi reconcile sets them against a provider's bill row by row, and the doctor's activation check carries the paid period (paidUntil) and the monthly fee as fields. The command-line additions and the doctor's fields are experimental: outside the 2.x compatibility promise, they may change or go away in any 2.x release. Nothing here changes a default: tapeapi --help is 12 lines longer and the doctor's --json has more keys, but with monthlyFee() at 0, as on mainnet today, the doctor's text report is byte for byte what it was and the monitor's report gains no fee line, and an MCP request that does not name revision 2026-07-28 gets exactly the answer it got before. Nothing below makes or takes a payment, and a receipt still shows what its signer says, not which model ran.
Added
tapeapi receipts tail|show|exportandtapeapi reconcile(experimental: outside the 2.x compatibility promise, they may change or go away in any 2.x release). Both read files on this machine only: no network, no key, nothing uploaded.receiptsreads the JSON linestapeapi-verify --logwrites (or bare receipt envelopes):taillists the last calls with model, tokens, price and the logged verdict, filtered by service (container address, or the name in a manifest snapshot), model and time;show <id>prints one receipt and replays its checks offline (signature and signer, container, method and path, price table; the two hashes when--request/--responsegive the exact bytes) against a manifest snapshot saved next to the receipts (manifest.json,*.manifest.json) or given with--manifest, taken as given: the line under the verdict says its signer was not checked on chain there, and--json'sreplayhassignerCheckedOnChain: false; without one it decides nothing (exit 3) and fetches nothing;exportwrites CSV or JSON with stable columns.reconcile --receipts <file|dir> --bill <file>sets the usage and prices the receipts claim against a bill file row by row and lists three kinds of fact, each with its receipt line and bill line: match, missing (on one side only) and mismatch (a field that differs from what the receipt claims); it decides nothing. Rows join by an id column, else by time, model and token counts.--preset new-apinames the fields of the JSON new-api'sGET /api/log/selfreturns (data.items:upstream_request_id,model_name,prompt_tokens,completion_tokens,quota,created_at), taken from new-api's source code, not yet checked against a real instance; new-api's web CSV export is not that file (localized headers, a formattedCostthat is not a comparable amount,request_id), does not match the preset (exit 2) and, like other bills, names its columns with--map; amounts are compared only with--amount-scale(new-api: itsquota_per_unit). Exit status 0 / 1 / 2 / 3 as the doctor's; every non-zero exit ends with the next command;--json,--lang en|zh|both; output redacted astapeapi connect's, except the hashes, ids and addresses a receipt carries. Files:sdk/src/receipts-cli.js,sdk/src/reconcile-cli.js(no new export subpath; theaiface is unchanged).tapeapi --helplists the two commands and their exit status (12 more lines);tapeapi doctor|verify|mcpandtapeapi connectare unchanged.tapeapi-doctor: theactivationcheck carries the paid period and the fee as fields (experimental, like the doctor itself: outside the 2.x compatibility promise, they may change or go away in any 2.x release). In--json(and indiagnose()'s report) the check haspaidUntil(ISO 8601, UTC),paidUntilSeconds(DomainBinding.containerPaidUntil(container)as read, Unix seconds, compared on chain with the block time) andmonthlyFeeWei(monthlyFee()in wei, a decimal string), eachnullwhen not read, with the reason inunread(absent when the check is skipped or undecided); a script no longer needs to parse the date out ofdetail.nulldoes not mean expired: a script reads the check'sstatusfirst and excludesnullbefore comparing a date (new Date(null) < nowis true in JavaScript). The doctor now readsmonthlyFee()for an activated name too (one moreeth_call); when it is not 0, the text report prints onenote:/提示:line under the check. The check's status, itsdetail, the status names and the exit codes are unchanged; withmonthlyFee()at 0, as on mainnet today, the text report is byte for byte what it was.api.siteStatus()andtapeapi connectread nothing more.- MCP:
resultType, andttlMs/cacheScopeon list results, for a request of MCP revision 2026-07-28.createMcpServerin@tapeapi/sdk/mcp, and socreateMcpEndpoint,createMcpProxyandtapeapi-mcp: a request whoseparams._meta['io.modelcontextprotocol/protocolVersion']names revision 2026-07-28 or a later one getsresultType: "complete"on its result, and the result oftools/list,resources/list,resources/templates/listandprompts/listalso getsttlMs: 0andcacheScope: "public". A request without that key (every request of revisions 2025-11-25, 2025-06-18 and 2025-03-26) gets exactly the answer it got before;initializeand error answers never change. Nothing signed changes: the fields sit beside the result, so receipts, signed envelopes andtoolsSha256are the same in both revisions, and the proxy talks to its upstream as before. The endpoint does not otherwise speak 2026-07-28 (noserver/discover, no header checks). The CORS preflight already allowsMcp-MethodandMcp-Name(2.0.0). Checked against the official specification on 2026-10-14, when 2026-07-28 was its latest revision: https://modelcontextprotocol.io/specification/2026-07-28/basic/index (results andresultType) and https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching (ttlMs,cacheScope). - The monitor (
scripts/monitor.mjs): its implementation-slot check is described indocs/OPERATING.mdand tested end to end on the SDK's fake chain, and it notes amonthlyFee()that is not 0. The slot check is not new: since 2.0.0 the monitor reads the ERC-1967 implementation slot of the SiteRegistry and DomainBinding proxies throughapi.siteStatusand compares it with theexpectedImpllist insdk/src/chains.js; an unlisted implementation is aSTORE-CHANGEDALERT and exit code 1, and slots that cannot be read are a warning only. New:docs/OPERATING.mdsays so; four tests (MON-2.2-1toMON-2.2-4) run the monitor's own default client over the TAP-10 fake chain with a proxy's implementation swapped (alert, exit 1), with the older listed DomainBinding implementation (quiet), and witheth_getStorageAtfailing on every node (warning, never an alert); and the warning for unreadable slots now ends "whether either was changed is undetermined". For an activated service the monitor now also readsmonthlyFee()(the doctor'sfeeWhenLive: 2 more read-onlyeth_calls per run, one per activated service): a fee that is not 0 (it is 0 on mainnet today; the monitor keeps no state between runs, so "no longer 0" is judged against 0) adds one line under Notes with itsmonthlyFeeWei, and a fee that cannot be read adds one that says it is undetermined; at 0 the report is byte for byte what it was. In--jsonan activated service'sactivation.feeWeiandfeeTextnow carry the fee (they werenull), andfeeUnreadappears when its read failed. The status names, the alerts and the exit code are unchanged: the fee line is a note, never an ALERT or a problem.
[2.1.0] — 2026-10-17
TapeAPI 2.1.0: tapeapi connect, one command that shows whether a service can be used, how to set up a client for it, and why not when it cannot. Two entries change a default. The example sidecars' FORMATS (under Changed): set, it limits the formats published and refuses the others; a deployed 2.0.0 new-api sidecar or Worker without it keeps serving its four formats, with a warning. The fix under Fixed: ai.createVerifyingFetch in strict mode, its default, refuses a call to a format the manifest does not declare (strict: false keeps the 2.0 pass-through). Everything else below is experimental: outside the 2.x compatibility promise, it may change or go away in any 2.x release, and the 2.0 default resolution (TAP-10 / TAP-11 §2) is unchanged. Nothing below makes or takes a payment: tapeapi connect pays nothing, the price converter's prices are for display only, and the new-api local demo (chain-demo.mjs) runs on the SDK's fake chain only.
Changed
- Example sidecars:
FORMATSinexamples/new-api-sidecarandexamples/ai-proxy: unset runs asallwith a warning; set explicitly, it limits the formats. Set, only the API formats it names (openai-chat,openai-responses,anthropic-messages,openai-embeddings, comma-separated, orall) are published in the manifest and get receipts. A metered path of a format left out (POST /v1/messageswithoutanthropic-messages, also written loosely) is answered HTTP 400bad_pathwithx-tapeapi-sidecar-error: 1, naming the format andFORMATS, and is never forwarded: it can get no receipt, so it is never answered silently. Unmetered paths (/v1/models,count_tokens) pass through as before. An unknown name keeps the sidecar (or the Worker) in setup mode, namingFORMATS; so does a model whoseformatslists a format not enabled. Unset (a deployed 2.0.0 sidecar's.env, or the Worker's variables, after the upgrade), the sidecar runs asall, the four formats 2.0.0 published, and says so: aWARNINGline in the startup log (FORMATS is not set, so every format is published and signed (...)) and awarningmember in/tapeapi/v1/health(the Worker: in health, and once per isolate in its log), namingFORMATSand its values. The setup guides andenv.examplestill ask you to set it: only the formats the upstream really serves (FORMATS=openai-chatfor an OpenAI-compatible-only relay), orFORMATS=all.examples/litellm-sidecargainsFORMATSas a new, optional variable (its 2.0.0 server does not read it): unset, every format is published, as in 2.0.0; set, only those are, and a metered path of a format left out is refused as above. Its README,env.exampleanddocker-compose.yml(which now passesFORMATSto the sidecar) say so. @tapeapi/server/ai-proxy: optionrefuseOtherFormats(experimental, off by default): a metered path of a built-in format thatformatsleaves out is answered HTTP 400bad_path(with the sidecar-error header) and not forwarded, instead of passing through without a receipt;{ hint }ends the message. The example sidecars set it.- Wording fixes in documentation, comments, help text and vector descriptions (no behaviour, hash, signature or encoding changes).
docs/guides/container-agents.md(en, zh-CN): the release whose kit resolves the 2.0 way by default is 2.0.0, not 1.9 (never released). The TAP-13 binding rules are in §4 (Response envelope), not §8: the citation is fixed in that guide, inscripts/gen-vectors.mjs, inspec/vectors/verify.pyand in the public vector filespec/vectors/container-agent.json, whose content (and SHA-256) therefore changes in two description strings only; no input, expected value or check changes. The comments ofsdk/src/agent-*.js,review-*.jsandgroup-public.js,sdk/types/agent.d.tsand the help oftapeapi-verify tasksay "2.x compatibility promise" instead of "1.x", sotapeapi-verify task --helpprints that line differently.
Fixed
ai.createVerifyingFetchandtapeapi-verifyrecognise every built-in format (a fix in the stable SDK that changes a default): a metered path of a format the service's manifest declares no endpoint for, sent under one of its endpoint roots, used to pass through with no report, even in strict mode. Strict, which iscreateVerifyingFetch's default (strict: true), now refuses it withINVALID_ARGUMENTbefore sending (data: { format, declared }), as it refuses an endpoint mismatch: such a call can get no receipt, so passing it through silently contradicted strict. Code that relied on the old pass-through passesstrict: false: the call is then sent andonReportgetsmismatch: trueandnot verified: format not declared.tapeapi-verify --strictanswers HTTP 400format_not_declaredwithout forwarding; without--strict(its default) it forwards the call and logs it as FAIL, counted among the failed calls of its totals.
Added
tapeapi, one command-line entry, andtapeapi connect <name>(experimental; outside the 2.x compatibility promise). New bintapeapi:tapeapi doctor|verify|mcp …runtapeapi-doctor,tapeapi-verifyandtapeapi-mcpas child processes with the arguments as given, so their output and exit status are byte for byte the same (tested); those three commands are unchanged and stay.tapeapi connect <name>resolves an AI service withconform: 'tap10'(the 2.0 default), shows it (container, holder, signer, delegation, activation, formats, the price table, USD for display only) and prints client settings only for the API formats its manifest declares (ai.endpoints[].format): Claude Code only withanthropic-messages, Codex only withopenai-responses. A service that isunpaid,not-openedorstore-changedis shown as unavailable with the reason (exit 1) and--proxystarts nothing; for a usable service--proxythen runstapeapi-verifyas it is, which resolves the same way (the 2.0 default;connectnever passes--no-conform);--json,--lang en|zh|both; exit status 0 / 1 / 2 / 3 astapeapi-doctor(2 also when--tool/--formatnames a format the manifest does not declare; 3 when the chain, or the--devmanifest, cannot be read), and every exit other than 0 ends with the next command. It asks for no key or password, uploads nothing and pays nothing. Its output shows as***the value of a secret-named environment variable of this machine (*KEY,*TOKEN,*SECRET,*SIG, ...; not one that reads as ordinary text: under 8 characters, with a space, or a plain word under 16) and any key-shaped string (sk-...,0xand 64 hex digits, a long hex run, the token afterBearer); the rest of the manifest is shown as it is. New experimental subpath@tapeapi/sdk/connect(snippetsFor,connectReport,renderConnect). Documented insdk/README.mdand the AI providers guide ("What your users do"). Boundaries: a receipt does not prove which model ran; the settings are not tested against each tool's current release.examples/new-api-sidecar: a price converter and a local demo of the whole path.pricing-convert.mjs(tapeapi-newapi-prices; not part of either package) turns new-api'sGET /api/pricing(saved to a file) into the sidecar'smodels.json, with a source record and--checkfor drift; the exchange rate has no default and must be confirmed. USD per 1M tokens =model_ratiox 1000000 /quota_per_unit, read from/api/statusor given with--quota-per-unit(no default; new-api's own is 500000);create_cache_ratiogivescacheWrite. The prices are new-api's own quota dollars, published asUSDfor display only, not what a user really pays; tiered and video models are refused with a reason, andmodel_ratio37.5 without a billing mode (new-api's placeholder, or a real price) is refused as ambiguous unless it is a default-table price or--accept-ratio-37.5names it; the refusal says when self-use mode bills it. Only the groups the answer shows (usable_group) count, and who fetched the list (--fetched-as) is recorded.--checkcompares a hash of the price content; new-api'spricing_versionis a build marker, so its change alone is a note, not drift.chain-demo.mjsruns a fake new-api, the converter, a sidecar withFORMATS=openai-chatand a name on the SDK's TAP-10 test chain, thentapeapi-doctorby name in both resolutions (conform: 'tap10', the default, andconform: false), the SDK's checked calls, a tamper check and the converter's drift check; it proves the pieces fit on one machine, nothing about a real new-api, and no real chain.
[2.0.0] — 2026-10-12
TapeAPI 2.0.0: standards by default. With no options, createTapeAPI() resolves a service as TapeOut's official TAPs say (TAP-10 v1.1 §3 to §7 and TAP-11 §2), with one known exception (below); TAP-10 and TAP-11 are Drafts (a merge is not an adoption), and the status names that follow them stay experimental. No wire format or frozen constant of TAPI-20 to TAPI-27 changed: a 2.0 client verifies every 1.x manifest, delegation, signed response and receipt. What to change is in docs/guides/upgrade-2.0.md. 1.x receives security fixes for 6 months after 2.0.0. Bounded agents, US-dollar prices and reviews are new and experimental; the escrow they rely on is not audited and not deployed, so bounded agents and paid calls do not work on mainnet.
Breaking changes
createTapeAPI()resolves as TAP-10 and TAP-11 §2 say by default (the conformance mode,conform: 'tap10'). Without any option, every read is pinned to one block (TAP-10 §5.3), the chain id and the holder are read strictly (every node must answer and agree), and the states TAP-10 names are reported asdata.statusof the error:not-opened,unpaid,store-changed,unavailable,unsupported.conform: false(ornull) gives the 1.x resolution back; it is deprecated, kept for all of 2.x, and warns once when the client is made (the warning links the upgrade guide).dev: truealso meansconform: false. A configuration that is not a TapeOut chain's (a hub, factory or registry of your own, a local chain, apinother than'tap10') now fails when the client is made, with a message that says to passconform: false; nothing falls back silently.allChainsstays off by default.tapeapi-mcp,tapeapi-verifyandtapeapi-doctortake--no-conformand print which resolution they use;tapeapi-doctorreports a name that is not activated as a failure in the default (a warning with--no-conform). The agent kit resolves this way too (see Changed). Measured on BNB Smart Chain with the default nodes: a cold resolution is about 2 times and a warm one about 4 times the JSON-RPC calls of 1.x. Error codes that change, request counts and the checklist for service providers: docs/guides/upgrade-2.0.md.- A resolved manifest is kept for 60 seconds by default, instead of 3,600 (TAP-11 §7.1 says a client SHOULD NOT keep it longer than 60 seconds). A long-lived client reads the manifest about 60 times as often;
conform: falsekeeps 3,600 seconds. - A directory label is no longer an input to
resolveby default (TAP-10 §3.4): it is refused before anything is read, withINVALID_ARGUMENTanddata.status: 'input-error', where 1.x resolved it through the directory (or failed withMANIFEST_INVALID,DELEGATION_INVALIDorNOT_FOUND). Pass the name, the container or{ circuits, tokenId, chainId }instead;conform: falsekeeps the 1.x behaviour. - A manifest that begins with a byte order mark, or is not valid UTF-8, no longer resolves by default. 1.x resolved it; the 2.0 default refuses it as TAP-11 §3.1 and TAP-10 §16 say, with
MANIFEST_INVALIDanddata.status: 'manifest-invalid'. The holder publishes the manifest again as valid UTF-8 without a byte order mark; until thenconform: falseis the stop-gap. The upgrade guide lists it under "New failures". CONFORM_PROFILEandsvc.conform.profile(only added):'[email protected]+tap11@draft', the TAP texts the conformance mode resolves by (TAP-10 version 1.1, TAP-11 Draft). Absent withconform: false;siteStatus, error data and every existing field are unchanged. The upgrade guide now states the known exception to TAP-11 §2.1 (withoutallChains, a processor contract#ID string isunsupported, TapeAPI's own name), and the type declarations no longer say that the default's errors are unchanged.- How the default follows changes in the official TAPs (wording only; no behaviour, no label changed). TAP-10 and TAP-11 are merged Drafts, so their text can change. A loosening or a new status name becomes the default in a 2.x minor release; a tightening is offered first as an optional new profile and becomes the default in 3.0; status names only grow.
data.status,SiteStatusandsvc.conformstay experimental in 2.0.allChainsstays off and the known exception stays recorded. The upgrade guide has the section "How the default follows changes in the official TAPs", and its promise about status names now says exactly this. - Private channels and groups use TAPI-26 v2 and TAPI-27 v2 by default: labels that begin with
tape-channel/andtape-group/instead ofTAP-26/andTAP-27/. 1.8.1 added version 2 as an option (labels: 'v2'); 2.0 makes it the default, andlabels: 'v1'selects version 1 (deprecated in 2.x, not removed in 2.x). The TAPs editors asked for the rename before merging the drafts of private channels (TapeOutProtocol/TAPs#12) and private groups (#20); TAPI-26 and TAPI-27 are Stable (v1), whose labels TAPI-1 §4.1 freezes, so the rename is a version 2 of each. A 2.0 client and a 1.x client that passes nolabelsdo not interoperate (nothing fails open: they time out, or the SDK names the mismatch indata.labels/data.peerLabels); passlabels: 'v1'on the 2.0 side to talk to them. A group keeps its version for life: a 1.x snapshot (v: 1/v: 2) resumes as a version-1 group, a default group's snapshot isv: 3.api.chain.channelKeys(...).inbox.roomis now the version-2 inbox room. Channel records, relays and ChannelBus are unchanged. Vectors are unchanged (version 1's files byte for byte, version 2's from 1.8.1); the default-mode golden trace changes in one value, that inbox room. - Specs. TAPI-26 v2 and TAPI-27 v2 are Stable (v2) from 2.0.0 (their
Statuswas Draft before). TAPI-26 and TAPI-27 (Stable (v1), unchanged) note that from 2.0.0 they are no longer the SDK's default and that 1.x keeps security fixes for them for 6 months after 2.0.0. TAPI-1 §4.1 defines Stable (vN) and theTAPI-<n>-vN.mdfiles. No RFC 2119 keyword count changed. TapeAPIErrorno longer has the deprecated top-level aliases (e.tooLarge,e.rpcCode,e.rpcRevert,e.rpcData,e.agreed,e.disagreed,e.failed,e.groups,e.quorum,e.reason,e.refusals, ...): reade.data.*, where every detail has been since 1.0. The type declarations no longer list them.- The channel module reports a mistake in the caller's own options or arguments as
INVALID_ARGUMENT, like every other module since 1.0 (createInvite/acceptInviteoptions, a closed or misused session,relayTransport,busTransport,busReader,fanIn,sealToInbox/openFromInboxoptions, under either label version). TAPI-26 data that fails (invites, handshake messages, frames, sealed invites, including one of the other label version, and keys and relay lists read from them) staysCHANNEL_INVALID. The group module keepsGROUP_INVALID. - The provider directory and the monitor say what a 2.0 default client concludes. The daily directory recheck still runs
tapeapi-doctorwithconform: false, and its flag (ok,failing,stale,undecided) and the job's exit status mean what they meant; each entry insite/directory/status.jsonnow also hasdefaultMode, the site status a client in the default mode reads withapi.siteStatus(ok,not-opened,unpaid,store-changed, orunavailablewhen the chain could not be read or the name's cap of 60 RPC requests, shared with the doctor, ran out). The field is only added: it never changes the flag, never fails the job and never removes an entry;providers.jsonis unchanged. The directory page shows adefaultModethat is notokon the entry's card, with its reason.scripts/monitor.mjsmakes its client with the SDK default (a test locks it) and reports per service the default resolution's outcome (resolved, or the status it refused with); the 14-day delegation problem (exit 1) is unchanged, also when the default mode refuses the service. sdk/src/channel.js, frozen through 1.x, is unfrozen: it takesserviceitself (the publicrelayTransporthas takenservicesince 1.0 and no longer translates it) and readse.data.*instead of the aliases.
Added
- Prices in US dollars, paid in BEM (experimental; no escrow is deployed, so nothing is charged): the TAP draft "Payment Vouchers for Container Services" (TapeOutProtocol/TAPs#52), TAPI-22 §3.6. Only a manifest with the new top-level member
usdPricingis affected; any other manifest answers and pays byte for byte as before (server/test/payment-golden.test.mjs, recorded on the 1.8.1 payment code). New SDK namespaceusdPricing(validateUsdPricing, exactconvertUsd=min(ceil(U·10^18/R), priceBEM),validateQuote,readReferenceRatesover v3 pools at one agreed block,chargeRecord/checkChargeRecord);sig.recoverVoucherSignerandrecoverAddress(…, { strictV })refusev0/1 for vouchers while envelopes keep accepting them. Provider (createProvideroptionsusdRate,quoteValidS,quoteStore, newmemoryQuoteStore()): quotes honoured by every instance sharing the store, the draft's checks withdata.reason, and the signed charge recordchargein every priced result. Client (createTapeAPIoptionusdPricing): quotes only from verified envelopes, the reference check before signing,ratein the voucher, the charge-record check with a halt until a reread and an F2 read, priced calls of one channel one at a time (whatever svc objects or api instances share the payer), and consent to a USD price rise (maxPriceUSD); the provider gives quote requests their own budget (rateLimit.quote) and namespacesmemoryQuoteStoreby service (put(quote, container),get(rate, container)). Both sides takesettlementFacts(F1–F4 from configuration until a settlement TAP exists). Vectorsspec/vectors/tapi-22-usd.json, reproduced byverify.py. TAPI-22 §3.5: the first custody asset is BEM (decision of 2026-10-08), with its read-only admission check.
createRpctakesrpcTimeoutMs, the namecreateTapeAPI, itschainsandcreateProvideralready use for the timeout of one RPC request.timeoutMsstays; passing both with different values isINVALID_ARGUMENT. The channels guide uses the new name.api.tx.setContributionAll({ circuits, tokenId, bps, escrows })(experimental): onesetContributionper escrow instance, since each instance keeps its own setting.escrowsdefaults to the instances the client builds for (AUDITED_ESCROWS, empty today, thenallowEscrows; not theescrowoption, which onlysetContributionuses); none at all isINVALID_ARGUMENT, never an empty list, and so isescrows: null(the errors say both). Thechain.escrow.contributionsandtapeapi-doctor --escrowswarnings name it.- The conformance mode reads the hub's
Upgradedlogs (TAP-10 §13.8, a conditional SHOULD; experimental). In the conformance mode (the default since 2.0, orconform: 'tap10'),chain.tapeSendKeyalso asks, at its pinned block and under strict agreement, for everyUpgradedlog of the hub since its creation block (new constantsTAP10_HUB_HISTORY,TAP10_HUB_BOOT, read back on chain). A log naming an implementation that is neither the boot implementation nor one TAP-10 lists for the chain (current or replaced) ishub-changedeven when the implementation slot shows the current one again, withdata.upgradedLog; it is kept, insealStatusStoretoo, while the SDK's list does not accept it (a stored record of another shape is ignored). Only answers that hold the history's anchors count. When the logs cannot be read (the default public nodes do not serve this history) or nodes disagree, the hub is judged by its slot as before andtap10.hubsays so (basis: 'slot',upgraded.status: 'unavailable', retried after 300 s); otherwisebasis: 'slot+logs'. Withconform: false, exactly the same requests are sent as in 1.8.1. @tapeapi/sdk/agent(experimental; checked on local chains only, not usable on mainnet): phase-1 rules for mandates that name funds. Nothing changes by default: without the new option every check is phase 0 exactly as in 1.8, and no type hash, digest or existing vector changed. With{ enforcement: 'escrow-channel' },verifyMandateandverifyTaskThreadcheck such a mandate by static rules (scope-token-cap-mismatch,fee-token-cap-mismatch,scope-duplicate-provider,mode-scope-mismatch,subdelegate-not-allowed; a warningscope-includes-agent, whose static half, new functionphase1Warnings, is reported whatever the static problems) instead ofphase0-no-funds, then read every capped provider's escrow channel at the check's pinned block, the escrow address too (from the provider's manifest as it stands at that block) (escrow-not-allowed,escrow-other-chain,escrow-token-mismatch,session-missing,session-expiry-mismatch,withdraw-pendingby the request's lifetime of 48 h + 7 d,channel-empty,channel-exceeds-cap; with a fee,agent-key-on-fee-channelandfee-channel-short), and reportenforcement: 'escrow-channel'with each channel's readings only when all pass. The cap is the console's funding target, not a bound: what the chain bounds is always the channel's balance, every phase-1 result says so withbound: 'channel', and a channel above its cap fails the check. New kit optionallowEscrows, new optionsfundingPath(holder, the default, or container) andfeeDue, new functionwithdrawRequestLive.mandateTypedDataandforWallettake the same option and show a phase-1 warning and every amount. New pure functionescrowPlan: the holder's unsignedapprove,fundandauthorizeSessiontransactions, from the holder's wallet or through the principal container'sexecute(the TapeOut fee supplied by the caller). It refuses inputs that contradict each other (one escrow given with two tokens, a provider that is the plan's consumer or an escrow of the plan) instead of taking the last one.escrowPlanbuildsapproveandfundfor the token the caller names (checked against the scope and the escrow'stoken()): they do not go through 1.8'stx.fundand its BEM-only escrow check. That is for checking this experimental subpath on local chains; it does not lift the BEM restriction. New vectors:phase1inspec/vectors/container-agent.json. The escrow is not deployed or audited and pricing in other tokens is not specified, so none of this works on mainnet.@tapeapi/sdk/agent(experimental; local chains only, not usable on mainnet): the holder's plan reads its facts from the chain, with a close-out and a session history. Nothing changes by default. New kit methods:holderPlanreads, at one pinned block, everythingescrowPlanneeds (the principal's current holder, each provider's escrow from its manifest at that block, the escrow's token, the channel, a live request, the agent key's session) and refuses a holder that is not the current one (holder-not-current); on the container path it also refuses a principal that is not deployed or not opened (principal-not-deployed,principal-not-opened) and finds the TapeOut fee by simulatingexecute(no fee is written in) under amaxExecuteFeethe caller must give (a fee above it isexecute-fee-too-high; the plan says the fee times the number of calls in whole coins), refusing when the call still reverts with it (execute-reverts). A key already authorised on a channel until another time iskey-already-authorised; one authorised until the mandate's end gets no newauthorizeSession, and a plan run again after it was applied never funds back what the key spent: such a channel below its cap ischannel-spent, unless the session history holds the first run'srecord(its newclaimedAtPlan, the channel'sclaimedOfthen), in which case only the cap minus what the key has spent since is funded, and a channel above that ischannel-exceeds-budget; without that record, a channel at its cap on which anything was settled ischannel-spenttoo. The cap holds per mandate and provider over every channel the session history records: when it names this mandate's key on another channel to the provider (the provider moved its escrow, the circuit changed hands, a second wallet), the plan isfunded-elsewhereuntil that channel is closed out to 0 from its own wallet (one of another wallet never counts as closed), and then funds only what the key has not spent on any of them, and when nothing is left of the cap a fresh channel isbudget-exhausted(no loneauthorizeSessionon an empty channel); a lost history cannot be rebuilt for an escrow the provider no longer names. SoholderPlanandcloseOutrequirehistory([]when the console has no record; left out is an input error), and the pure functions say inchecked.historywhether one was given at all. Eachrecordentry carries the pinnedblock; a plan pinned at or before a record's block ispin-not-after-record(re-run only once the earlier run's transactions are final). With the signed mandate (signed, or itssigner), a holder that is not the one who signed it isholder-changed-since-signing, history or not.closeOut(new pure functioncloseOutPlan):requestWithdrawof each whole channel, thenwithdrawinside its window.afterPlan(new pure functionplanApplied): the allowance, channels and sessions read back after the plan, what the key spent since counting toward the cap, and why a channel is short (a failed fund, or a withdrawal that needs nothing). NewsessionHistory(one entry per session; a later record only fills a missingclaimedAtPlan, never replaces one) andsessionEntry, and the kit'ssessionLogs(only the given consumer's logs, up to the kit's pinned block): with a history, a plan refuses a key used for another mandate (key-reused) and a channel another key can still spend on (channel-session-live). A plan refuses a mandate the other funding path already funded (other-path-funded), and on the container path a holder without the native coin for every fee (holder-short-of-fee, from a balance read under strict agreement); the pureescrowPlanneedsholderon both paths, and the pure functions take anexecuteFeeonly with amaxExecuteFee.closeOuttakes the plan'srecordashistoryand refuses a consumer that did not fund (consumer-unknown), says when the consumer was not checked, and withfullRequest: truereplaces a live request for part of a channel by one for all of it. New constantsDEFAULT_FUNDING_PATH(the one default of every funding path option; which path it should be is not decided yet) andFUNDING_PATHS,EXECUTE_FEE_SELECTOR. New vectors:phase1.closeOutandphase1.applied, morephase1.plancases. The plans were executed on a local copy of the escrow only.@tapeapi/sdk/agent(experimental; local chains only, not usable on mainnet): the agent runtime's spend limiter. Nothing changes by default. NewcreateSpendLimiter({ store })wrapsapi.payer({ consumer, sessionKey })for an agent key: per mandate and provider, no voucher whose cumulative is above the channel'sclaimedOfat the check that started the work plus the cap is ever signed out (a refused one is released, never returned); that start never moves up; the escrow and consumer of the start are pinned, and a voucher for another escrow, consumer or chain is refused and stops the order; the voucher is checked to be the one counted; the record is written before the voucher leaves, and a key without its record (a restart without the store) never signs.accept,start(the same mandate again keeps its start; a stopped order stays stopped, and a check of another escrow or consumer stops it),recheck(a later check: a live request, or a result that is not an enforced phase-1 check with every channel reading (spend-not-enforced), refuses the step; another escrow, consumer or mandate, a revoked or expired one stops the order),payer,record,stop; pure rulesspendDecision,spendRecheck,spendPin,spendRecord, constantsSPEND_REFUSALS,SPEND_STOPS. One writer per record is the premise: records carryseq, every write re-reads the record and never overwrites one that changed, a store whoseset(key, value, { ifSeq })compares and sets closes the race between limiters sharing it, and a stop is also kept under a key of its own that no other write can erase. A voucher it lets out carries the unsigned membermandateHash(providers ignore unknown members), and the signed mandate until a voucher to that provider was accepted; a channel the last check read short of the price signs nothing (spend-channel-short). New vectors:phase1.spend,phase1.recheck.examples/agent-servicetakes an optionalspendoption that uses it (without it, the example is phase 0 as before; a revocation it receives stops the record);@tapeapi/serveris unchanged. Checked end to end on a fake chain and, for the transactions and vouchers, on a local copy of the escrow only.@tapeapi/sdk/agent(experimental; local chains only, not usable on mainnet): a delivery's spend statement and its reconciliation. Nothing changes by default. A phase-1 check already carries each capped channel'sclaimedAtCheck; a deliver result may now carryspend, the agent's signed statement, per capped provider, ofclaimedAtStart(claimedOfat the check that started the work),claimedAtDelivery(at its last check before it signed) andspent(their difference). It is covered by the delivery's signature, not byreceiptsHash. New pure functionsspendStatement(the agent's side: a channel it could not read on the pinned escrow for the pinned consumer is left out, never given as 0) andspendEvidence, constantsSPEND_EVIDENCE_PROBLEMS,SPEND_EVIDENCE_WARNINGS.verifyTaskThread(..., { enforcement: 'escrow-channel' })reconciles a delivery's statement against the applied mandate's readings at the same pinned block and against the receipts that verified, priced from the providers' manifests;verifyEvidencetakes{ enforcement, scope }for the same. Only a malformed statement, a provider without a cap, or amounts that do not add up are problems (spend-evidence-malformed,spend-provider-out-of-scope,spend-evidence-inconsistent); every mismatch is a warning inevidence.spend.warnings(spend-escrow-changed,spend-above-chain,spend-above-cap,spend-exceeds-receipts,spend-below-receipts,spend-evidence-incomplete), becauseclaimedOfmoves only when a provider settles (up to 48 hours later), a hash-only receipt does not sign its method, and prices are read from today's manifest in BEM. Without the option, or for a mandate without funds,spendis ignored exactly as before; with both and nospendmember at all, the delivery is warnedspend-evidence-absent(never a problem). New vectors:phase1.statement,phase1.evidence.examples/agent-serviceadds the statement to a spend order's delivery; a phase-0 delivery is unchanged.@tapeapi/sdk/agent(experimental; local chains only, not usable on mainnet): a default per-call price ceiling for the spend limiter. WithoutmaxPrice, the limiter's payer now refuses a call that costs more than 10% of the (mandate, provider) cap, rounded down to the token's smallest unit (new constantSPEND_DEFAULT_CALL_PRICE_PERCENT); where that rounds down to 0 there is no default and every call is refused until one is given (spend-call-price-ceiling-unset). AmaxPricegiven (one amount, or one per provider:{ [provider]: amount }) may be above 10% but never above the cap (spend-call-price-ceiling-above-cap). A price above the ceiling isspend-call-price-above-ceiling, which replacesspend-price-above-max; none of the three stops the order, and each message says how to set the ceiling. An explicitmaxPriceof 0 (in any form, or for a provider), an array or an object that is not a plain one isINVALID_ARGUMENT.examples/agent-servicepasses itsspend.maxCallPricethe same way and checks it when the service is made. Morephase1.spendvectors.@tapeapi/sdk/agent(experimental; local chains only, not usable on mainnet): an agent runtime can resume its spend orders after a restart, with the agent keys kept sealed. Nothing changes by default: without a key vault nothing is kept and a restart voids every order, as before. NewKeyVaultinterface (seal,open; the operator's: a passphrase, a key of its own, a KMS) andpassphraseKeyVault({ passphrase }): scrypt (N 2^17, r 8, p 1, constantsKEY_VAULT_SCRYPT,KEY_VAULT_MIN_PASSPHRASE) and XChaCha20-Poly1305 with a random salt and nonce, the blob bound to its context (the chain and the agent key); a blob that does not authenticate throwsAGENT_KEY_VAULTand never yields bytes. Pure functionskeyVaultKey,keyVaultSeal,keyVaultOpen,keyVaultParse,keyVaultAad,spendKeyContext.createSpendLimiter({ store, vault })addskeepKey(only the sealed blob is stored),resume(on a new limiter over the same store: the record, the sealed key opened and matched, then a fresh check compared asrecheckdoes; pure rulespendResume,SPEND_RESUME_REFUSALS:spend-state-lost,spend-stopped,spend-key-lost,spend-key-unreadable,spend-key-mismatch,spend-resume-mismatch) andforgetKey; every stop destroys the sealed key. A stopped, revoked or expired order, or one whose provider moved, never resumes, and the ceiling (the channel'sclaimedOfat the start plus the cap) holds across the restart.examples/agent-servicetakesspend.vaultwith a persistentspend.storeand gainsrecover(); its newstate.mjskeeps the state in 0600 files and turns persistence on only withTAPEAPI_AGENT_KEY_PASSPHRASEandTAPEAPI_AGENT_STATE_DIR. New vectors:phase1.keyVault,phase1.resume. Whoever has both the passphrase and the files has the keys, so the worst loss stays the channels' balances.resumerefuses a missing or stopped record before the vault is asked to open anything; a vault seals at most 4 MiB, and a longer blob is malformed before any key is derived. In the example the whole order record (offer and task text, mandate, receipts, deliverable) is sealed by the same vault, so a record changed on disk is never signed as the agent's own (order-record-unreadable,order-record-corrupt); every method refusesrecoveringuntilrecover()has run;ctx.resumedtells a task what was paid before the restart; one runtime process per state directory (a lock file, taken over only when its process is gone on this host); and the state directory must never be restored from a backup or a snapshot (a rollback revives orders that were stopped, revoked or delivered).@tapeapi/sdk/review(experimental): evidence-backed reviews between containers, the reference implementation of the draft "Evidence-Backed Reviews between Containers" (TapeOutProtocol/TAPs pull request #54, not merged, no number), which may change. A review record is one EIP-712 typeReviewin the TAP-11 domain (chainId the reviewer's chain):makeReview,reviewHashOf,reviewTypedData(with what a console should show beside the wallet),signReview(the reviewer's holder or its delegate, the TAP-11 signer of its service),reviewRequestId(tape-review:<reviewer>:<nonce>, a label for the request id of a TAP-13 call, never the caller's identity),subjectOf,textHashOf,evidenceHashOf.createReviewKit(api)verifies records (verifyReview,verifyReviews) with the strict readers ofcreateAgentKit: both containers' identities and holders, EIP-1271 and the site file at one pinned block per chain, the reviewer's delegate and the reviewee's signer as TAP-11 §2 resolves them, another chain throughapi.forChain; level 1 (a signed answer whose request id names the reviewer, within 90 days) and level 2 (a #47 task: mandate and verdict by the principal's holder, delivery by the agent's signer, the phase-0 rule) reuse the SDK's TAP-13 and #47 code. Every result gives the level,signedBy(holder,delegateor null),counted, the draft's problem names and whether the text is withheld; a chain or node failure throws, never a problem. Carriages:readReviewFile(.well-known/tapeapi-reviews.json, at most 262,144 bytes, a larger declared size is not read, records naming another reviewer ignored;reviewFileBytes) andreadReviewMessages(public TapeSend messages with areviewmember from the reviewer, final only, the block time in place of now, the sending wallet marked when it differs from the current holder;reviewMessageContentrefuses a payload over 16,000 bytes).summarizeReviews: only records signed by the current holder or delegate correct, withdraw or conflict; one record per pair; level 1 and level 2 apart; distinct reviewers and holders, never amounts.checkReview(record, facts)replays the draft's vectors (spec/vectors/tap-draft-container-reviews.json, 59 cases and 6 summaries, a byte copy) with given chain facts;verify.pyrecomputes their hashes and signers independently.tapeapi-verify review <reviewee>(experimental subcommand, not in the main--help) prints the records of the reviewers' site files, the reviewee's inbox and a records file, and the summary.
Changed
@tapeapi/sdk/agent(experimental; local chains only, not usable on mainnet): in the spend evidence,spend-above-capnow uses the verifier's own reading (spentNow) when it has one, and the agent's reportedspentonly otherwise; one more vector,phase1.evidence35 to 36. The SDK and the independent Python implementation change together.
createAgentKitresolves the agent and the providers as TAP-11 §2 says by default (@tapeapi/sdk/agent, experimental). Without aresolveoption the kit now resolves them through a conformance-mode client (conform: 'tap10': one pinned block per resolution, strict reads) made from the kit's own client, with the same nodes, quorum, RPC timeout,fetchand other options; it never turns onallChainsand reads no other chain, so no request goes to a node the client was not given. A client already in the conformance mode is used as it is. The results change where TAP-10 does: a container that is not opened or not activated, or a site store or payment contract running an implementation TAP-10 does not list, is nowagent-unresolvableorprovider-unresolvableinstead of resolving. A client whose contracts are not the ones TAP-10 lists for its chain (a local or test deployment) keeps its ownresolve, as in 1.8. With aresolveoption nothing changes. New propertykit.resolution:'tap10','client'or'caller'.tapeapi-verify taskbuilds its kit this way too. To keep the 1.8 default, passresolve: (container) => api.resolve(container).- The
/mcpCORS preflight also allowsMcp-MethodandMcp-Name(createMcpEndpoint,createMcpProxy, and so the public service and the proxy examples), so a browser client of a newer MCP revision can send its requests. The headers allowed before are unchanged. The guide on MCP now says which revisions the endpoint speaks.
[1.8.1] — 2026-10-08
Added
- Private channels and groups: optional version-2 labels (
labels: 'v2'). At the request of the TAPs editors (TapeOutProtocol/TAPs PRs #12 and #20), the domain-separation labels of private channels and private groups have a version 2 that begins withtape-channel/andtape-group/instead ofTAP-26/andTAP-27/, with the same suffixes. The SDK implements it as an option: passlabels: 'v2'tochannel.createInvite,acceptInvite,roomsFor,inboxRoom,sealInvite/sealToInboxandopenInvite/openFromInbox, and togroup.createGroup,joinGroup,openGroupInvite,groupRoomandcheckGroupInvites. The default is unchanged: without the option the SDK uses version 1 exactly as 1.8.0 did, and the version-1 vector files are unchanged. Version 2 is planned to become the default in 2.0.- Both sides must use the same version. Invites carry no version (they keep
v: 1), and the two versions do not interoperate. A mismatch fails closed: the rooms differ, and an accept, ready, sealed invite or epoch message of the other version is refused withCHANNEL_INVALID/GROUP_INVALID,data.labelsanddata.peerLabels. The SDK never switches versions on its own; any otherlabelsvalue isINVALID_ARGUMENT. For a sealed invite the error says only how the wire was sealed: anyone can post to an inbox room, so it is no evidence about the sender. - Pending handles, sessions and group handles report
labels. SDKs before 1.8.1 ignore thelabelsoption silently and use version 1; an application that passeslabels: 'v2'should checkpending.labelsandsession.labels(orgroup.labels), which those SDKs do not have. - The
snapshot()of a version-2 group isv: 3withlabels: 'v2'(SDKs up to 1.8.0 refuse it), andresumeGrouptakes the version from the snapshot. Av: 1orv: 2snapshot that sayslabels: 'v1'is accepted, as 1.8.0 accepted it; one that sayslabels: 'v2'is refused. - Specifications:
spec/TAPI-26-v2.mdandspec/TAPI-27-v2.md(Draft).spec/TAPI-26.mdandspec/TAPI-27.mdstay Stable (v1), with a non-normative note. New vectorsspec/vectors/tapi-26-v2-channel.json,tapi-26-v2-identity.json,tapi-27-v2-group.jsonandtapi-27-v2-group-format2.json, written by the SDK and checked byspec/vectors/verify.pywith the version-1 files.
- Both sides must use the same version. Invites carry no version (they keep
[1.8.0] — 2026-10-06
Changed
@tapeapi/sdk/agent(experimental): checks follow more of the draft TAP (TAPs PR #47) and are stricter. The subpath is outside the 1.x compatibility promise; messages, threads and evidence that 1.7.1 accepted may now be refused, and some problem names changed. No type hash, digest or existing vector changed.- Input forms are refused, not read leniently (draft §3.7, §7.2, §8). A message received from a counterparty must carry every
bytes32in lower case, everyuint256(nonce,fee,feeCap, a scopecap) as a decimal string (a JSON number or a bigint is nowmandate-malformed/message-malformed), and a verdict'sreasonHash(missing ornullis nowmessage-malformed; 1.7.1 read it as zero). An agent message whosetsorresult.expis negative, or whose receipt lacks the TAP-13 method,paramsobject or 65-bytesig, or whoseidis not 1 to 128 code units (TAP-13 §3), ismessage-malformed. A hash-only evidence receipt whoseokis not a JSON boolean (1.7.1 read it asfalse), or whoserequestHash,bodyHashortsis not in its form, or whoseidis not 1 to 128 code units (the emptyidof TAP-13 §8 binding rule 1 is allowed only whenokisfalse), isreceipt-not-hash-only. The builders (mandateTypedData,taskOfferTypedData, the hash andsign*functions) still take a number or a bigint and still default a missingreasonHash;bytes32input must now be lower case there too. New pure functions:agentMessageProblem,hashOnlyReceiptProblem;normalize*take{ wire: true }. - One pinned block per verification (draft §4, §6.2; TAP-10 §5.3). Every read of
verifyMandate,verifyTaskThread(both passes),verifyEvidence,readRevocationsandidentityOfis made at one TAP-10 pinned block, by block hash: the principal's identity and holder, EIP-1271 calls, both implementation slots and the revocation file. 1.7.1 read each atlatest. The kit checkseth_chainIdunder strict agreement once;stale-blockandwrong-chainare thrown withdata.status, never reported as problems. - The principal is resolved as TAP-10 §4.3 says. The container is derived by the chain's container opener (1.7.1 asked the hub), and the processor number is found on the chain (snapshot hit confirmed by
cpuAt, else a bounded scan), so the on-chain name is always present (1.7.1 gavenulloutside the snapshot). Every identity failure is nownot-tapeoutorno-such-token: the problem namesnot-a-containerandwrong-chainare gone (atoken()naming another chain isnot-tapeout). - Resolving the agent or a provider. A site status (
not-opened,unpaid,store-changed) from the resolver, or a conformance-mode client'sunsupportedfor an address that is no container of the chain, is nowagent-unresolvable/provider-unresolvable(1.7.1 threw it);stale-block,unavailableandwrong-chainare thrown. The kit still resolves through the client'sresolve; the guide shows how to pass aconform: 'tap10'client (TAP-11 §2) as the kit'sresolve. - The two passes share reads, the total goes up. The two passes of
verifyTaskThreadshare one read of the revocation file, the implementation slots and each identity (within one thread the kit's own revocation-file and slot reads (pathCount,fileInfo,readand the slots) are half of what 1.7.1 made); the slots are kept per pinned block for at most 60 s. The total number of reads still rises: resolving the principal adds the pin (eth_blockNumber,eth_getBlockByNumber), oneeth_chainIdper kit and, outside the snapshot, acpuAtscan (at most 256 reads per processor looked up, per verification, resumed on the next one). - Vectors:
spec/vectors/container-agent.jsongainsinputForms, 66 hand-written cases checked by the SDK and byspec/vectors/verify.py.
- Input forms are refused, not read leniently (draft §3.7, §7.2, §8). A message received from a counterparty must carry every
- Payments (experimental, TAPI-22; no escrow is deployed or audited): escrow amounts use the escrow's own token, and
approve/fundare built only for allowed escrows. Outside the 1.x compatibility promise.api.tx.approveandapi.tx.fundnow return a Promise. Both read the escrow'stoken()and the token'sdecimals()under strict agreement first, and are built only for an escrow on the newAUDITED_ESCROWSlist (empty) or in the newcreateTapeAPI({ allowEscrows })option; any other escrow is refused (INVALID_ARGUMENT,data.reasonescrow-not-allowed), the configuredescrowincluded.approveapproves the escrow's own token instead of defaulting to BEM; itstoken, and the newfund(provider, amount, { token }), name the token you expect.fundis stricter thanapprove: it is refused for any escrow that holds a token other than BEM (UNSUPPORTED_PAYMENT_TOKEN,data.reasonnot-bem), whatever token you name and even for an allowed escrow, because the escrow contract itself takes any admitted token but the manifest's pricing in more than one token is not yet specified, so this SDK does not build funding for a channel it cannot use yet.- New client error code
UNSUPPORTED_PAYMENT_TOKEN(data.reason:token-unreadable,decimals-unreadable,decimals-out-of-range,not-bem,token-mismatch). Manifest prices arepriceBEM, sofund, a pricedapi.calland anapprovewithout a named token (or for a service) refuse an escrow that holds another token; the escrow contract itself takes any admitted token, and prices in other tokens are not yet specified. Free methods and dev services are not checked. - Inside the SDK and the server, every
parseUnits/formatUnitscall names its decimals. The functions' own default (8, BEM) is unchanged.
- Escrow contract v3 (experimental, TAPI-22; the candidate for audit): never deployed and not audited, so no live channel, provider or fund is affected.
contracts/src/TapeAPIEscrow.solis the first version that holds one ERC-20 token per instance by design. This is outside the 1.x compatibility promise, and nothing here says the audit will keep this shape.- The first constructor argument is the immutable
token(it wasbem), andtoken()is the view. The contract never reads the token's decimals; the deploy script requires 8 to 18. - The solvency statement reads
token.balanceOf(escrow) >= sum of channels + treasuryAccrued(it was==): tokens sent straight to the escrow stay in it and are never paid out. No code path changed for this; the statement and the invariant test did. - The maintenance contribution is pulled, not pushed.
settleadds it to the newtreasuryAccruedand makes one token call, to the provider; the newclaimTreasury()(anyone may call it) pays the accrual to the current treasury and emitsTreasuryClaimed. A treasury that cannot receive the token no longer blocks any settlement.Settledkeeps its signature; itscontributionnow means the amount accrued. setTreasurypays the outgoing treasury first (a failed payout is rolled back and ignored, so a frozen treasury never blocks the change; the unclaimed accrual then belongs to the new treasury), and it refuses the escrow itself and its token as treasury (new errorBadTreasury(), also in the constructor).- One judge for token calls: every token call that does not answer success (a revert, a malformed or short return, a non-bool word) reverts
TransferFailed(); the return data copied is bounded to 32 bytes. contracts/script/Deploy.s.solchecks before it sends anything: the canonical USDT-pegged token with its code hash pinned, no proxy token (EIP-1967 and EIP-1822 slots zero), any other token only withALLOW_OTHER_TOKEN=true, a treasury that is not zero, the token, the TapeOut hub or the container factory; the post-deploy checks readtoken()andtreasuryAccrued(). New tests, among them a fork rehearsal of that script (needsBSC_RPC_URL, skipped without it).- Voucher type, typehash, EIP-712 domain and every channel, session, withdrawal and rate rule are unchanged.
- The first constructor argument is the immutable
- TAPI-22 (Experimental) revised to specify the escrow that will go to audit. No escrow has ever been deployed, so no channel, provider or fund is affected. The 1.7.1 text of
settleand the treasury functions is replaced; an implementation of it would need to change them. Normative changes, in both the English text and its Chinese translation:settleaccrues the contribution totreasuryAccruedand MUST NOT transfer it to the treasury (§3.3; the 1.7.1 text required the transfer);claimTreasury()(anyone may call) pays the accrual;setTreasurypays the outgoing treasury first and refuses the escrow and its token (§3.4); voucher amounts are in base units of the instance's token (§3.1); every token call that does not answer success revertsTransferFailed(). The constructor argument and view are namedtoken, the solvency line reads>=(§3.3), and §3.5 and §7 record which planned items the repository contract now has. Counting everyMUST(MUST NOTincluded), each half goes from 40 to 47;MUST NOTalone from 7 to 8.uptosettlement by measured usage is stated as not implemented and not specified, and is not part of the audit scope.
Added
- Experimental, for escrow v3 (not deployed):
api.chain.escrow.token,treasuryAccrued,paymentToken(token and decimals read on chain, cached; the label from the newPAYMENT_TOKENS, never the token'sname()) andcontributions(one provider's contribution across escrow instances, with a warning when they differ);api.tx.claimTreasuryandapi.tx.wrapNative({ wbnb, amount })(WBNBdeposit(), the wrapper address from the caller, no gas field);formatPaymentAmount;abi.EVENTS/abi.eventTopic(Settled,TreasuryClaimed) and ABI entries fortoken(),treasuryAccrued(),claimTreasury(). tapeapi-doctor --escrows a,b(anddiagnose({ escrows })), experimental: notes when your contribution differs between escrow instances. No new check; the exit code is unchanged.scripts/verify-bscscan.mjstakes--tokenfor the escrow's first constructor argument;--bemstill works and prints a deprecation notice.
Fixed
@tapeapi/sdk/agent(experimental):viaContainerhad no way to attach the TapeOut fee, so its transactions revert on BNB Smart Chain mainnet. A read-only test of mainnet at the time of writing found that a TapeOut container'sexecute(address,uint256,bytes,uint8)needs the fee in the chain's native coin on every call (0.0002 BNB, sent to the treasury, the excess refunded to the sender; a shorter payment reverts with0xafd49700(paid, required)), andviaContainerin 1.7.0 and 1.7.1 always built the outer transaction with value 0.viaContainer({ from, tx, value })now takes an optional outervalue(a decimal string, a safe integer or a bigint, 0 or more; anything else isINVALID_ARGUMENT), and without one the value stays0x0and the summary carries a warning that the call may revert without the fee; with one, the summary says it is the TapeOut fee and goes to TapeOut, not to the recipient. The fee has no getter: the SDK neither reads nor hard-codes it (simulate the transaction and read the revert data). The inner value is still always 0 and a non-zerotx.valueis still refused. Types and the container-agents guide are updated.
[1.7.1] — 2026-10-05
Changed
@tapeapi/sdk/agent(experimental): revocation in a task thread follows the draft TAP (TAPs PR #47, §7.5-§7.6). The subpath is outside the 1.x compatibility promise; these change what 1.7.0 reports.- A revocation, whether a
revocationmessage in the thread (wherever it sits) or the principal's site list found while checking the applied mandate, now only sets the thread's revocation time R, the smallestissuedof the revocations that apply. It never changes the state when met. An agent message signed after R is refused (message-after-revocation); a verdict is allowed whatever itsissued; only the final checks turn a thread stillOffered,AcceptedorActiveintoCancelled, beforeExpired, and only once R is not afterat. Because R depends on the applied mandate, which depends on the accept, the thread is read twice and the second reading's problems are reported. - In 1.7.0 a site list covering the mandate made the thread
Cancelledat once withmandate-revokedand every later messageout-of-order, even a delivery and verdict signed before the revocation; a revocation message cancelledOffered,AcceptedorActivewhen met, and was accepted silently inCancelled. Now a thread delivered and accepted before the revocation still readsSettled, by either path, and the same revocation gives the same result as a message or as a site list.mandate-revokedis no longer reported in a thread (verifyMandateon its own still reports it). - A revocation found while checking a mandate that the thread refuses (for example
nonce-reused) no longer sets a revocation time. tapeapi-verify taskprints the new result for such threads, and itsrevoked: yes, at N (via)line is nowrevocation: at N (via); agent messages signed after it are refused(orrevocation: none): the revocation time refuses later agent messages and does not by itself mean the thread was cancelled. Therevokedmember of a thread result keeps its shape and now means that revocation time.
- A revocation, whether a
@tapeapi/sdk/agent(experimental): the principal's revocation file is read as the draft's §6.2 says. A missing file isfileInfo.chunkCount0 (it wassize0; TAP-10 §7.1); the file is read from the first site store of the chain that has any path for the principal (TAP-11 §2.2 step 3), and only while the site store and the payment contract run implementations TAP-10 lists as accepted (TAP-10 §6.1; otherwise the list is invalid, so every mandate of that principal isrevocation-unavailable, as for any unreadable list); a file that begins with a byte order mark is invalid (it was silently stripped).createAgentKitnow refuses, when it is created, a chain the SDK does not know or a site store or payment contract with no implementation TAP-10 lists as accepted (INVALID_ARGUMENT, as the conformance mode does), instead of reportingrevocation-unavailableon every check.- Test vectors:
spec/vectors/container-agent.jsongainsthreadRevocation, fifteen abstract thread cases whose results are worked out from the draft, the boundaries included (an accept or a delivery signed exactly at the revocation time is allowed; a revocation time equal toatcancels, one second later does not); the SDK is checked against them on real signed threads andverify.pychecks them with an independent model of §7.4-§7.6 (514 to 561 checks).
[1.7.0] — 2026-10-05
Added
@tapeapi/sdk/agent(experimental): container agents, phase 0. A new subpath, outside the 1.x compatibility promise (the formats follow the public Ideas TapeOutProtocol/TAPs#40 and #41 and may change with that discussion). Phase 0 has no enforcement: a mandate is a holder's signed statement, every check saysenforcement: 'none', a mandate naming any amount is refused, and a payment is a plain transfer verified read-only (TAP-10 §19). It adds four holder-signed EIP-712 types (Mandate,TaskOffer,TaskVerdict,MandateRevocation; domain "TapeAPI", hub asverifyingContract),createAgentKit(verify a mandate, a task thread and an evidence bundle; read revocations;identityOf),createPaymentKit(payment orders,transferToContainer,viaContainer,recipientOf,describeTx, and a 15-step payment check),revocationFileBytes, andforWallet(td, { chainId, hub }), which every typed-data result must pass through before it reaches a wallet (it stripswarningsanddisplay, and rejects a payload whose chain or hub is not the one the console expects). A task is the message sequence offer, accept, mandate, deliver, acceptance, revocation. Test vectors:spec/vectors/container-agent.json(the vector set goes from 480 to 514 checks;verify.pyagrees with the SDK).validateAgentMemberand the manifest's optionalagentmember (experimental).examples/agent-service/: a runnable agent service and a hiring script (offline demo world with a fixed clock).tapeapi-verify task <thread.json> [--payment <recipient> <index>] [--rpc <url>...]: check a task thread (and, with--payment, the payment) from the command line. The first argumenttaskused to be an error and is now this subcommand; every other invocation behaves as in 1.6 (the output of ten argument shapes is pinned byte for byte).DEFAULT_CONTRIBUTION_BPS = 100is exported next toMAX_CONTRIBUTION_BPS.
Changed
- Maintenance contribution: the contract cap is 20% (was 50%), the default stays 1%.
MAX_CONTRIBUTION_BPSgoes from 5000 to 2000 in the SDK and incontracts/src/TapeAPIEscrow.sol;setContributionrefuses more than 2000. The escrow now keeps "never set" apart from "set to 0" (a provider set to 0 contributes nothing; one never set pays the default 1%), andSettledcarries thebpsthat was applied. TAPI-22 says the directory MUST NOT rank or badge services by contribution. The escrow is still not deployed and not audited, so no live channel is affected. Guides, FAQ andBUSINESS.mdnow say 20%. - Revenue share: the basis changes from 2026-10-05. The permanent 10% for @Theairresearch is now 10% of each calendar quarter's revenue after the direct cash costs paid to third parties (rented services and infrastructure, audit fees, gas, related compliance and legal fees; never labour), settled quarterly with public accounts of income, deductions and payout. Payments received before 2026-10-05 are still shared under the earlier promise (10% of any revenue). See
BUSINESS.md.
Fixed
formatUnitsreturned a wrong string for a token with 0 decimals (10showed as.1); the payment summary was the first caller to pass a decimals value read from a chain.
[1.6.0] — 2026-10-04
Added
createVerifyingFetch({ requestUsage: true })andtapeapi-verify --request-usage: the usage of a streamed OpenAI Chat answer can be checked (opt-in; off by default, and onlytrueturns it on). An OpenAI Chat stream carries usage only when the request setsstream_options.include_usage, and the officialopenaipackage does not set it (at the time of writing, 7.x). When a streamed request does not, the sidecar asks the upstream for the usage itself and takes the usage chunk out of the client's copy (usageInjected), so no client could compare the receipt's usage with anything (it was listed inunchecked, 1.5's "remaining by design"). With the option, the verifying fetch (or the proxy) setsinclude_usage: truein the request's bytes before sending: the sidecar then injects and strips nothing, sets nousageInjected, and the usage chunk is part of the signed stream the application receives, so the receipt's usage is compared with it as a whole answer's is. This changes no wire format, digest or vector. It does not prove the upstream's own count: it gives usage the standard a whole answer already has, that the receipt states what the application was handed, so an overstated usage is signed into an answer the application can see (attributable), and a receipt that disagrees with the stream fails (RECEIPT_INVALID; FIXED AI-ASK-1, AI-ASK-END). Details:- What changes in the request. Only for a request the sidecar would itself change (a stream OpenAI Chat request whose
stream_options.include_usageis nottrue), one splice in the bytes, every other byte kept: nostream_optionsgets,"stream_options":{"include_usage":true}after the last member; an object withoutinclude_usagegets,"include_usage":trueinside it; an existinginclude_usage(false,null,0,"true", ...) has its value replaced bytrue; astream_optionsthat is not an object (null, an array, a string,false) is replaced by{"include_usage":true}. The result is checked before it is used: it must parse to exactly what the sidecar's own rule (prepareUpstream) gives, and the sidecar's rule must have nothing left to change. The order is body, splice, salt,content-lengthremoved, send;requestSha256is the hash of the bytes finally sent. - What is left alone, and what then happens. A compressed body (
content-encoding), a Content-Type that is not JSON (content-type), a body that is not UTF-8 (not-utf8) or starts with a byte order mark (not-object), a format withprepareUpstreambut nousageMember(no-member), a key that appears twice at the top level or insidestream_options(duplicate-member; a duplicate deeper down is kept as it was), and a splice whose self-check fails (self-check; a body nested deeper thanJSON.stringifycan go is one). Withstrictthe SDK throwsINVALID_ARGUMENTbefore sending (data: { reason, format }) andtapeapi-verify --strictanswers HTTP 400 (usage_request_skipped, withx-should-retry: false) without forwarding; otherwise the request goes as it is, the report saysusageRequestSkipped: <reason>, and the usage stays inunchecked. A request that asks for usage itself, is not a stream, or is not an OpenAI Chat request is not touched (no skip reason). - Reports. With the option on, every report on a receipt path carries a boolean
usageRequested(false: this fetch did not set the member, because the application asked itself, it is not a Chat stream, or the splice was skipped), and possiblyusageRequestSkipped; with it off neither field exists.tapeapi-verify --logwrites the same fields, the verdict line saysusage=askedorusage-request-skipped=<reason>, and the start-up line says(usage requested). If the request asked for the usage and the stream carries none, there is a warning ("the request asked for the usage (requestUsage), but the stream carries none: the upstream did not send it, or something on the way took it out"). If the receipt states a usage the stream lacks, that is a failure as before; if it states none, it passes with usagenull. - What an application sees. One more chunk before
[DONE]withchoices: []and the usage, as when it setsinclude_usageitself; nothing is taken out on the client, which is why the bytes it holds are the bytes the receipt hashes. Code that readschunk.choices[0]without a guard throws aTypeErroron that chunk; the.stream()helper of theopenaipackage works, andfinalChatCompletion().usagehas the value. Claude Code (Anthropic Messages) and Codex (Responses) streams carry their usage already and are not touched; Embeddings do not stream. - Limits and risks (the reasons it is off). A JSON body sent with a non-JSON Content-Type is skipped by the client while the sidecar, which does not look at Content-Type, still injects: that call's usage is not checkable (strict refuses it locally). An upstream that ignores
include_usageleaves a verified receipt with usagenulland the warning above. A sidecar that does not inject (the reference has no switch for it; it takes a customformatstable whose Chat adapter has noprepareUpstream), because its upstream rejectsstream_options, will see these calls fail with HTTP 400 once clients turn this on. A third-party sidecar that strips the usage chunk although the request asked for it fails understrict. The salt now also reaches the upstream for these requests (the sidecar's re-serialising used to drop it): it is whitespace after the JSON text, which JSON allows, and is not yet measured against the live APIs. Understrict, the official SDKs may wrap theINVALID_ARGUMENTas a connection error and retry it up tomaxRetriestimes (not measured; no request is sent in any retry). - Interfaces. New:
requestUsage(createVerifyingFetch),usageRequestedandusageRequestSkippedon theonReportreport,usageMemberonAIFormat(['stream_options', 'include_usage']foropenai-chat; a custom adapter without it is never rewritten),UsageRequestSkip, andai.requestUsageBody(@internal, used by the sidecar andtapeapi-verify). With the option off nothing new runs and the default client path is byte for byte 1.5.0's (GOLDEN AI-1.5, recorded on 1.5.0 before this change). The guide has the three ways to get a stream's usage checked.
- What changes in the request. Only for a request the sidecar would itself change (a stream OpenAI Chat request whose
Fixed
- The reference sidecar re-serialised the request it sent upstream (FIXED AI-RESER; every version with usage injection). To ask the upstream for the usage of a streamed Chat request, it sent
JSON.stringifyof the parsed body, which changed more than that one member:seed: 12345678901234567890reached the upstream as12345678901234567000,1e400becamenull, a repeated key was merged, whitespace and escapes were rewritten and the client's salt was dropped, against TAPI-21 §3.5 ("MUST NOT change … in any other way"). It now setsinclude_usagein the client's bytes with the same function the client uses (above), so every other byte arrives as sent; receipts are unchanged. Where the splice refuses, the sidecar re-serialises as before: a key repeated at the top level or insidestream_options(the repeated keys are merged), a byte order mark, a Content-Type that is not JSON, a custom format withoutusageMember. This needs@tapeapi/sdk1.6:@tapeapi/servertakes the function from the SDK's root entry, and with an earlier SDK installed it falls back, silently, to re-serialising (the server's dependency range moves to^1.6.0with the release). - Three smaller fixes around
requestUsageand the sidecar (FIXED AI-ASK-CASE, AI-RESER-SDK). A key that differs fromstream_options(at the top level) or frominclude_usage(insidestream_options) only in letter case, after the JSON escapes are decoded, now makesrequestUsageskip the request (duplicate-member): a gateway whose JSON decoder ignores case and lets the later member win (new-api and one-api are Go programs) would otherwise readinclude_usageas false. The sidecar logs one line when@tapeapi/server1.6 runs with an@tapeapi/sdkolder than 1.6 (the request is then re-serialised the way 1.5 did, so the fix above does not apply; the server's dependency range is raised to^1.6.0with the release). And a streamed Chat request nested thousands of levels deep, which made the sidecar answer HTTP 500, now gets its HTTP 400request_too_deep(withx-should-retry: false; settinginclude_usageyourself sends it on as it is).
Security
- Release integrity. Every GitHub Action in the workflows is pinned to a commit SHA (with its tag in a comment), and a test keeps it so. From v1.6.0 on, every Release carries a
SHA256SUMSfile next to the two tarballs, made byscripts/release-checksums.mjsfrom the uploaded files, and the release notes repeat it. SECURITY.md gains §5, "Verifying a release": how to check, what a checksum on the release page proves and what it does not, and why there is no build attestation (releases are built by hand, not by CI).
Documentation
- The AI providers guide gets "The usage of a streamed Chat answer: three ways", with what each way guarantees and what it does not. TAPI-21 §3.5 (usage injection) gets one explanatory paragraph in both languages: a client can ask for the usage itself, and the sidecar then changes and strips nothing and sets no
usageInjected. It adds no MUST, SHOULD or MAY and changes no frozen constant or vector. The verification page's note on an unchecked usage now says the client can ask for it.
[1.5.0] — 2026-10-03
Added
- TAP-10 conformance mode: the messaging path (experimental). Under
createTapeAPI({ conform: 'tap10' }),api.chain.tapeSendKeyresolves the endpoint as TAP-10 §12.2 says and reads the key as §14.4 steps 1–3 say, andapi.chain.channelKeys(sogroupVerifiertoo) reads the channel record under the same rules: a fresh TAP-10 pinned block per lookup, strict agreement on every read (every configured node, all equal, at least max(2, min(3, operators)) operators) and oneth_chainId(§5.4), the container from the opener.tapeSendKeyalso checks the hub at that block (§13.8): an implementation other than the current one TAP-10 lists ishub-changed, a circuit beacon off its implementation iscircuits-changed(kept by the client), and the seal status is reported inresult.tap10.seal; the key must belong to the resolved container and endpoint (hub-mismatch), be usable (no-key,key-stale) and pass the X25519 checks (bad-key).channelKeysneeds both site-store implementations accepted (store-changed) and refuses a record with a byte order mark. Neither judges activation or opening (§12.2): the same unactivated container isunpaidtoresolveand has its record and key read as usual. One node down or behind the pin now stops these reads in this mode on BNB Smart Chain's three default operators (strict; Base's four leave one spare, X Layer's two need no more than the quorum), not in the default mode, which is unchanged. Also new:tapesend.endpoint,seal,open,messageIdandsendTxtakeconform: 'tap10', which refuses a chainId above 2^53 − 1 (§12.1);TAP10_SEALS(the beacon, circuit implementation and current hub implementation of each chain, read back on chain 2026-10-02) andTAP10_MAX_CHAIN_ID. Input that may belong to another chain (an address that is no container here, atoken()naming another chain, a processor contract that is no processor here) isunsupported, as inresolve, and never cached. A client withconform: 'tap10'now accepts only the hub, the processor factory and the container opener TAP-10 lists for its chain (hub,factory,openerset to anything else areINVALID_ARGUMENT, TAP-10 §2.2), a tightening of the experimental mode of 1.4 forfactoryandopener. The new optionsealStatusStore(a{ get, set }store, likechannelRecordFloor) keeps the sticky §13.8 statuses across restarts; without it they last as long as the client. The guide has the statuses and codes. - TAP-10 conformance mode: the rest of the resolution path (experimental). A container address or processor contract#ID now finds its processor number (TAP-10 §4.3), so it gets its on-chain name and a full activation check (
isLiveas well asisContainerLive): the SDK ships each chain's processor table (sdk/src/processors-snapshot.js, read-only through the default nodes before each release; on 2026-10-01 (UTC; the snapshot's times are UTC) BNB Smart Chain 1,174, X Layer 263, Base 101 processors, each recorded with its block, count and node operators), uses a hit only after onecpuAtat the pinned block gives back the same address, and otherwise scanscpuAtat the pinned block in pages of 8, at most 256 numbers per resolution, resuming at the next one (unavailablewithdata.scanuntil then); only the numbers after the snapshot are scanned once the chain agrees with it, so a cold client does not send a thousand requests to one node. The new optioncreateTapeAPI({ allChains: true })resolves a container address or processor contract#ID string on every TapeOut chain, each at its own pinned block (TAP-10 §4.1): two chains resolving it isINVALID_ARGUMENTwithdata.statusambiguousanddata.candidates; a chain that cannot be read, or whose site contracts arestore-changed, is reported with its own status (anddata.chainId) instead of a guess ornot-tapeout; results and these errors carrychains, what each chain said (siteStatus(...).chains,svc.conform.chains,data.chains). It sends reads to the nodes of Base and X Layer, which is why it is off unless asked: onlytrueturns it on, any other value is ignored. It applies toresolveunderconform: 'tap10'and tositeStatusin any mode, never to the defaultresolve, andforChainpasses it on. With it, all 23 TAP-10 Test Cases thatnode scripts/tap10-gap.mjsjudges offline conform (18 in 1.4). The guide has the rules;resolveand thechain.*reads of the default mode are unchanged (GOLDEN TAP10-0).resolvein this mode now reads what would authorise a signer under strict agreement (TAP-11 §2.2, TAP-10 §5.2):ownerOf, and for a holder that is a contract the EIP-1271 calls (eth_getCodeandisValidSignature, for the delegation and forcontentSig); itseth_chainIdcheck is the strict one the messaging path makes, before any state is read. Strict needs every configured node to agree, from max(2, min(3, operators)) operators, and a node that does not answer or has not reached the pinned block yet (TAP-10 §1: "no such block" is no answer) counts against that. On the default nodes: BNB Smart Chain (3 operators) now stops with one node down or behind the pin where 1.4 resolved; Base (4 operators) still resolves with one node down, not with two; X Layer (2 operators) is unchanged. WithcontentSigpresent andrequireContentSigoff, a contentSig check that fails this way is the warningCONTENT_SIG_UNCHECKED, as before. Every other read of a resolution, andsiteStatus(its chain check included), keep default agreement; a strict read'sRPC_UNAVAILABLEnow says how many operators it needed. With the messaging path above, this closes the resolve and messaging paths of the mode; the hub'sUpgradedlog scan (TAP-10 §13.8, a conditional SHOULD) is not done.
Changed
- TAPI-26 erratum: an invite sent by TapeSend is sealed to the recipient's usable TAP-10 key, read from the hub, never to the recipient's channel key: TAP-10 §14.4 and §15.3 allow a sender no other key, and TAP-10 then governs the message completely. §3.2 said "sealed to B's static key". The durable fallback therefore reaches only a recipient that also holds its TAP-10 key (a TAP-10 client, or a channel with
keys: "tapesend/v1"); a TAPI-26 client with channel keys alone (keys: "tape-channel/v1") cannot receive it, and §3.1 already asks other implementations not to derive a TapeSend key. §5 records the correction: no implementation had sent or received such an invite, and no code changed (the SDK never built one). Six section references in §3.1 and §3.2 (both languages) now follow TAP-10 v1.1 (§14.2; §15 and §16; §16). Keyword counts and frozen constants such as theTAP-26/…labels are unchanged. - Input without chain information, and
siteStatusin any mode. Underconform: 'tap10'withoutallChains, a processor contract#ID string (0x…#ID) is nowINVALID_ARGUMENTwithdata.statusunsupportedbefore any request, because TAP-10 §4.1 resolves it only when exactly one chain does (in 1.4 it was resolved on the client's own chain when that chain had the processor);{ circuits, tokenId, chainId? }resolves it on one named chain as before. A container address is still resolved on the client's chain when it is a container of that chain, and isunsupportedotherwise; the error now namesallChains: true. The 1.4unsupportedfor a container or processor contract#ID whose processor number was unknown is gone, along withactivation.isLive: null.siteStatus, in any mode (it is the TAP-10 path on a default client too), changes with it: a container address or processor contract#ID gets itsnameandprocessor(null in 1.4) and a cold client readscpuCountandcpuAtto find them; an object form that names its chain ({ circuits, tokenId },{ chainId, container }) gets that chain'snot-tapeoutwhere 1.4 saidunsupported; on a default client a processor contract#ID string is still resolved on the client's chain, as in 1.4.
Security
Streamed AI receipts: four related fixes to ai.createVerifyingFetch and tapeapi-verify, with matching changes to the offline check (verifyUsageReceipt({ responseBytes }), scanSse, the verification page) and the sidecar. The receipt hash (TAPI-21 §3.5) and its test vectors are unchanged; TAPI-21 §8 gains an informative note on streams that parsers read differently, with no new requirement.
- A stream cut short could verify (1.0.0–1.4.0). Both clients check a stream where it first ends (its final event,
[DONE]or the upstream closing, whichever comes first), but they took the receipt hash, and let the format adapter read the answer, only after processing the whole network chunk that carried the end. Events after the end that came in that same chunk were counted as part of the checked answer, although a client that stops at the end (the official OpenAI SDKs stop at[DONE]) never sees them. So a party on the path (a proxy, a CDN, or the service itself) that moved the receipt comment earlier and inserted a[DONE]could hand the client an OpenAI Chat or Responses stream cut off before its real end that was reported as verified, and that strict mode released, whenever the inserted[DONE]and the rest reached the client in one chunk.tapeapi-verifywithout--strictchecked the receipt against the whole stream when the upstream closed, so it logged such a stream as verified however it was cut. Anthropic Messages streams, which have no sentinel, were not affected by this one. The scanner now keeps the hash, event count and offset where the stream first ended (info.digestAtEnd,info.eventsAtEnd,info.endOffset), the receipt is checked against them, and the adapter reads up to the end only. - Strict passes the stream on up to its end and closes it there. Strict used to release the whole chunk that carried the end, so whatever followed the end in it (a comment, an event without data, half an event, a second
[DONE]) reached the application, and an event after the end in a later chunk failed the stream. Now the chunk goes on up to the end only, the stream closes and the upstream is cancelled (tapeapi-verify --strictends the answer there), so what the application receives does not depend on how the bytes were cut, save one byte: when the blank line that ends the stream is a CR at the end of a chunk, the stream ends at that CR and an LF that may follow is not waited for (every client dispatches the event without it, and an upstream that then sent nothing used to hang the stream). Not strict still passes every byte on and reports an event after the end once as a failure (an event after the end of the stream is not covered by its receipt), in the chunk that ended the stream or a later one; before,strict: falsereported nothing for it. - Two stream shapes that clients parse differently from the receipt rule now fail closed (all versions with streamed receipts). An event the stream closes on before its blank line is discarded by the rule, but the OpenAI SDK dispatches it when the body ends: content appended that way after a verified Chat stream with its
[DONE]removed, or after a Responsesresponse.completed, reached the application as verified. And a line that starts with U+FEFF (other than at the very start of the stream) is an unknown field to the rule, but both official SDKs, OpenAI and Anthropic alike, strip a byte order mark from every line and read it as a data line: content could be inserted anywhere in a signed stream of any of the three formats without changing its hash. Strict now ends such a stream withRECEIPT_INVALID(a line led by U+FEFF is not passed on),tapeapi-verify --strictwith its error event, and not strict reports each once as a failure. The offline check reports both shapes as problems too (scanSsenow returnsambiguousandunfinished; the verification page fails the response check and says why), so a stream that verified offline although clients were shown more than the hash covers no longer does. The reference sidecar no longer signs a stream with such a line before the point where it signs (one after it, past the end, is cut off by strict clients and reported by the others). A few rare honest shapes are refused as well, since clients would read them differently: a stream whose upstream broke off in the middle of an event, after which the sidecar added its receipt line; a stream that starts with two byte order marks; a line that is only a byte order mark; and a data line led by a byte order mark after a CR line end. tapeapi-verifywithout--strictcould log nothing. It checked the receipt only when the upstream closed, so a client that hung up at the end (the openai SDKs stop at[DONE]) while the upstream kept the connection open left no verdict. The receipt is now checked and logged as soon as the stream ends, and when the client hangs up after the end.
verifyUsageReceipt({ responseBytes }) over a whole stream hashes every event in it, as TAPI-21 §3.5 says, so for a stream with events after its end its verdict can differ from the streaming check. Tests replay signed streams of each format, honest and edited, under many cuttings (one event per chunk, whole, byte by byte, seeded random cuts, LF and CRLF), strict and not, and assert that the verdict, and in strict mode the bytes handed on, do not depend on the cutting.
Two more, from an adversarial review of the 1.5.0 candidate:
- A receipt could switch off its own usage check (every version with AI usage receipts).
verifyUsageReceipt, and soai.createVerifyingFetch, skipped the usage comparison whenever the receipt saidusageInjected: true, even when the client held a whole answer whose usage it could read. A receipt signed with the service's own key (which whoever runs the sidecar holds) could therefore claim any usage and matching prices and still verify against the answer it came with. The flag now counts only where the client's copy can lack the usage: a stream, of a format that injects (OpenAI Chat), for a request that did not itself ask for usage. A whole answer is always compared, and a stream whose request already asked, or of a format that never injects, that claims the flag is a problem. The honest sidecar's receipts all still verify, including the one for a stream request answered as JSON. The verification page compared only the hashes of pasted bytes; it now also reads the pasted answer with its format (a newanswercheck: id, model, usage, completeness), as TAPI-21 §3.5 check 4 asks. The wording of that check now says when the usage cannot be compared, with no new requirement. A verifier with no content type of its own (the page, andverifyUsageReceiptcalled withoutstream) took the receipt's ownstreamon trust, so a whole JSON answer under a receipt re-signed as a stream with the flag was read as a stream without usage and its usage skipped; such a pairing now fails (ai.isWholeJson; on the page, the response check says why).createVerifyingFetchwas not affected: it passesstreamfrom the response's content type. Remaining by design: the usage of an injected stream (a streamed OpenAI Chat request that did not setstream_options.include_usage) cannot be checked by anyone, since the client never receives the usage chunk; a client that asks for the usage itself avoids that. Having the verifying fetch ask for it is a candidate for 1.6. - Hidden text in MCP tool definitions (every version with the invisible-character check).
mcp.invisibleProblems, used by the signing proxy,tapeapi-mcpand the holder console, refused format characters (category Cf) and controls only. Variation selectors (U+FE00–FE0F, U+E0100–E01EF: 256 invisible code points, enough to spell any text) and code points that render blank although they are not Cf passed: every Default_Ignorable_Code_Point outside Cf, assigned or not (U+034F, the Hangul fillers, the Mongolian free variation selectors U+180B–180D and U+180F, U+2065, U+FFF0–FFF8, U+E0080–E00FF, U+E01F0–E0FFF, ...), the braille blank U+2800 and U+1D159. So a description or another pinned field could carry instructions a model reads and the holder approving the digest cannot see. They are now refused, except a single U+FE0E or U+FE0F directly after an emoji character ("⚠️" stays allowed); a battery of real descriptions in a dozen scripts, with emoji, keycaps, NBSP and combining accents, still passes. A tool set already pinned that uses any of them, an ideographic variation selector in Japanese text included, is now refused (INVISIBLE_CHARACTERS) until it is changed and republished.
Fixed
- AI sidecar: an
x-tapeapi-sidecar-errorheader from the upstream is no longer passed on (a client reads it as "the sidecar answered itself" and reports a transport failure for a signed answer). - AI sidecar: a path with an encoded
/or backslash (%2F,%5C, any case) is refused with HTTP 400bad_pathbefore anything is forwarded, since an upstream may decode it into a separator;ai.loosePathreads both as/, so the verifying fetch (andtapeapi-verify) reports the same URLs as a path mismatch (strict refuses them). - An
idwith a lone UTF-16 surrogate has no canonical form: the provider binds it as no id (a signedBAD_REQUEST, where it could fail with HTTP 500), andapi.call()refuses it withBAD_REQUESTbefore sending. - JSON-RPC: a single (not batched) answer whose
idis not the request's, or is missing ornull, now counts as a node that did not answer, as a batch element with the wrong id already did: anid: nullanswer that is not a node limit (a result, or a revert) is no longer taken as an answer about the chain. A node-limit refusal withid: nullstays a refusal. - MCP: an upstream
isErrorthat is not a boolean is refused before signing by the signing proxy, andtapeapi-mcprefuses such a signed result as not an MCP tool result: what was signed and what was shown could disagree (a signed"true"was shown as a success). Text imitating the provenance line is now labelled in an embedded resource'stextand a resource link'sname,titleanddescriptiontoo; the instructions namestructuredContentas data. - AI receipts: a usage count of
-0from the upstream is read as 0; the sidecar answered HTTP 500 because canonical JSON has no-0. canonicalJSONrefuses the holes of a sparse array (it wrote[1,,2], which is not JSON); the holder console's copy does the same.- With
clientIpHeaderset, the header's address is written one way per address (IPv6 compressed and lowercase, an IPv4-mapped IPv6 address as its IPv4, anything that is not an address"unknown"), so one client cannot take several rate-limit keys by spelling its address differently; the new-api and LiteLLM sidecar examples do the same. WithoutclientIpHeadernothing changes.
Documentation
- Calling services for people you do not trust (consume guide): the SDK does not refuse private or internal endpoint URLs, which are valid in a manifest; a server or agent that resolves services for others should pass
createTapeAPI({ fetch })a wrapper that refuses them (an example is given). - Upgrade guide: on a chain with two operators (X Layer's default nodes) one operator can hold a pinned read back as far as
maxPinAgeS(pin: true) ortap10MaxPinLag(TAP-10 mode) allows, and the client then reads the manifest as it stood then;delegationFloordoes not cover it. A third operator's node closes the window.
[1.4.0] — 2026-10-01
Added
- TAP-10 conformance mode: the resolution path.
createTapeAPI({ conform: 'tap10' })resolves on the TAP-10 v1.1 / TAP-11 §2.2 path (false,nullor leaving it out means off, as before), withpin: 'tap10'(also usable alone) and the read-onlyapi.siteStatus(target). The default behaviour is unchanged. In this mode:- the whole resolution is pinned to one block by TAP-10 §5.3: each operator's lowest head, the Q-th highest minus 2,
stale-blockby block distance (BNB Smart Chain 400, Base 150, X Layer 300), no clock; the block hash is then confirmed by every answering node (the SDK keeps its stronger pin by hash); - the site store's and the payment contract's implementations are read at that block and fail closed (
store-changed); - the container is derived through
opener.accountOfand checked against the local ERC-6551 derivation, andisOpenedand the activation of the name (isLive,isContainerLive; a revert counts as false, TAP-10 §6.3) are read: a name that is not opened or not activated fails with the new codeSITE_STATUS(data.status:not-openedorunpaid). Every error in this mode carriesdata.status, TAP-10's lowercase name; - the input forms of TAP-10 §3.4 are accepted (
#4246@0,tape://4246.0/,0x…#ID), an input with no processor number whose activation cannot be told is answeredunsupported(the all-chain lookup is planned for 1.5); eth_chainIdis checked; only a result or a revert counts as an answer (TAP-10 §1); the holder and the site state are read again after 60 s. Activation is judged byresolveandsiteStatusonly:channelKeysand the TapeSend key of an unactivated container are still read (TAP-10 §12.2). Today both11.1013.tapeand12.1013.tapeanswerunpaidin this mode: neither is activated. Also new:rpc.tap10Block,{ answers: 'tap10', strict }oncall/ethCall,parseTapeInput,MAX_TOKEN_ID,MAX_PROCESSOR,CONFORM_TTL_MS,tap10MaxPinLaginchains.js. The guide lists what stays for 1.5 and the limits (two-operator chains pin more weakly).
- the whole resolution is pinned to one block by TAP-10 §5.3: each operator's lowest head, the Q-th highest minus 2,
- The monitor reports each service's activation (TAP-10 §6.3): the paid-until date and days left, or the
monthlyFee()read at that moment when the name is not activated. Not activated, and under 14 days left, are warnings; under 3 days or expired is a problem. The exit code is otherwise unchanged (today both of our services are not activated, and the run is still healthy). It adds 6eth_callrequests per run to each node. tapeapi-doctorchecks activation (now 14 checks). It reads DomainBindingisLive(name, container)andisContainerLive(container)(a revert counts as false, TAP-10 §6.3). A name that is not activated is a warning, not a failure (a TAP-11 client answersunpaidand does not resolve the service;--strictmakes it fail), with thebindhint and the feemonthlyFee()reads at that moment. The guide has a new step 2b. Run against11.1013.tapeand12.1013.tapetoday it warns: neither is activated.node scripts/tap10-gap.mjs: a diagnostic that runs TAP-10's own Test Cases through the SDK's name resolution and prints where it conforms and where it does not (offline; of 23 cases that can be judged offline, 12 conform and 11 are known gaps, each with its TAP-10 section).sdk/test/tap10-gap.test.mjspins the current results (CONFORMS,KNOWN GAP), so the opt-in TAP-10 mode that is planned can flip them. Not part of the public SDK interface.
Changed
- Name range (erratum, all modes). Names are limited to #ID ≤ 10^18 and processor number ≤ 10^9 (TAP-10 §3.1); up to 1.3, 78 digits were parsed. Such names cannot exist on chain, so a name that used to cost lookups is now refused with no request. The error text for a name that was already refused changed with it.
- The status of the TAP drafts is stated: the service manifest and delegation was merged as TAP-11 (Draft, TapeOutProtocol/TAPs#8) and seven more drafts are under review (#10, #12, #16, #18, #20, #26, #28). TAPI-20 to TAPI-27 remain TapeAPI's own names and the basis of the 1.x compatibility promise.
- The specs are renamed TAPI-1 and TAPI-20 to TAPI-27 (they were TAP-1 and TAP-20 to TAP-27). TAP numbers belong to TapeOut's TAP process (TapeOutProtocol/TAPs, TAP-01 §6.1), where editors assign them and the multiples of ten are reserved for core standards; TapeAPI no longer uses any TAP number, and TAP-20 is left free. Parts of these specs are submitted there as TAP drafts: the service manifest (TapeOutProtocol/TAPs#8), signed responses (#10) and private channels (#12). Labels and constants that contain the old names (
TAP-26/…,TAP-27/…) are frozen wire constants and do not change: no wire format or signature changes. Conformance check ids are nowtapi20.…and so on. Earlier entries below use the new names.
Fixed
tapesend: an endpoint built from an address now sits on the recipient's chain.seal,open,messageIdandsendTxtake an optionaltoChainId(default:chainId, so existing calls are unchanged); before, thetoendpoint of a cross-chain message used the sender's chain, which a TAP-10 client reads as a damaged payload (TAP-10 §15.3, §17). The official message-ID vector #2 (hub on Base, recipient on BNB Smart Chain) reproduces.
[1.3.0] — 2026-09-30
Added
- Merkle proof mode (experimental, security roadmap 1.2; the light-client idea borrowed from Polkadot).
createTapeAPI({ pin: true, proofs: true | 'strict' })checkscpuAt,isCPU,ownerOfandfileInfo(size,sha256Hash) against EIP-1186eth_getProofproofs of the pinned block'sstateRoot, which nodes ofquorumoperators must agree on (rpc.confirmedBlock(tag, { stateRoot: true })); the manifest bytes are then checked against the proven hash. A proof may come from any node; everyeth_callstill goes through the quorum and is compared with the proven value.truewarns and keeps the quorum reads;'strict'refuses with the new codesPROOF_UNAVAILABLEorPROOF_INVALID. It resists nodes that agree on false state, as long as thestateRootcomes from independent operators; it does not resist an upgrade of the contracts (that is real state) or all operators forging a header. RLP and Merkle-Patricia checks are the SDK's own (no new dependency), checked against ethereum/tests trie cases by both the SDK andverify.py; vectorsspec/vectors/tapi-20-proof.json; TAPI-20 §3.2 note, §6.5;svc.proofs, theproofnamespace. Default unchanged. Which default nodes serve proofs (2026-09-30): Alchemy on BNB Smart Chain, dRPC on Base, none on X Layer at thesafeblock (add your own node to use'strict'there). A proven value that contradicts the quorum's answer is refused in both modes (PROOF_INVALID);proofs: truefalls back only when no verified proof can be had, and is then detection only. A proof that does not verify moves on to the next node; only a node that refuses the method or serves a bad proof is skipped for 10 minutes, never for a rate limit or a timeout. A confirming node that omits thestateRootis not counted (nodes ofquorumoperators must still report the same one). RLP nesting is capped at 64 andproof.*throws onlyPROOF_INVALID. Not proven:contentSig, EIP-1271isValidSignature,accountOf(the sentinel derives it locally) andserviceOf. An independent review (77,000 RLP and trie cases, attacks on the whole resolution) found no way to forge a proof. - Provider directory at https://tapeapi.fun/directory/. An AI service lists itself by a pull request to
site/directory/providers.jsononcetapeapi-doctorpasses;.github/workflows/directory-recheck.ymlrunstapeapi-doctoron every entry once a day (read-only, no key, at most 60 JSON-RPC requests per name) and commitssite/directory/status.json; an entry that cannot be read that day isundecidedand keeps its last verdict. CI checks the format only. A listing only means the automated checks passed: it is not a recommendation, an endorsement or an audit. No ranking, no fees, no automatic removal; TapeAPI lists nobody itself and pays for nothing.
Fixed
- Resolve on BNB Smart Chain, X Layer and Base, found by testing 1.2.0's security options on all three mainnets:
- A rate limit is the node not answering, never an answer, whatever its code or wording (Coinbase's
-32016and dRPC's code 15 inside a batch, HTTP 429, "too many requests", "quota"): on Base it used to surface asRPC_DISAGREE. A revert that happens to use those words is still an answer. - A pinned read that publicnode or Coinbase answers with "block not found: canonical hash" counts as that node lacking the block (only "not canonical" is still an answer).
- The pinned block may be older on X Layer (
maxPinAgeS300 -> 600 s; itssafetag moves about every 228 s, measured up to 248 s old) and must be newer on BNB Smart Chain (180 -> 120 s;finalizedmeasured 0 to 2 s old). - A request whose connection broke (not a timeout, not an answer) is sent once more after
transportRetryMs(default 250 ms): X Layer has two independent operators, so one reset used to fail the read. createTapeAPI({ quiet })reaches the clients it makes, so a default X Layer client no longer warns.
- A rate limit is the node not answering, never an answer, whatever its code or wording (Coinbase's
tapeapi-doctorand the AI relay path, found by walking the docs as a new operator:- The docs no longer say
tapeapi-doctorruns only from a checkout (1.2.0 ships it), and its "next" commands match how it was run: a checkout path, the npx form with the release URL, or the installed command. - URL mode probes the address you gave, and warns when the manifest publishes another one (it used to probe the manifest's endpoints, so a pre-publish check could pass against the wrong server).
- Diagnoses that fit: a page that is not the sidecar (a reverse proxy or wrong port), a self-signed certificate in plain words, and an opened container without a manifest (sidecar, key, delegation, then publish).
--lang en|zhprints one language (--jsonaddsdetailZh); usage errors are bilingual.- Step 0 says
npm cifirst, andexamples/relay-trialprints that command instead of a module-not-found stack. - PowerShell forms next to every shell variable example;
tapeapi-verifyneeds its own terminal.
- The docs no longer say
- The public relay's place is stated in the Public API guide: it is for testing and small-scale use (Cloudflare's free tier, about 100,000 requests a day for all users); apps with always-online users or groups over about 64 members run their own relay at their own cost.
- Website: the status line reads "Stable 1.x"; every "up to 32" mentions the experimental format 2 (up to 128); the verify page no longer repeats its hash-only notice; docs links have no
.html, and the sitemap lists the upgrade guide.
- Groups, format 2 (experimental), found by testing 1.2.0 across versions and on the public relay:
- The owner no longer checks every member again at each epoch: it reuses its positive verdicts within
verifyReuseS(a new option oncreateGroup/resumeGroup; 0 checks all) for entries that did not change, and a new member is checked once. Rotating or removing one member of a 128-member group went from 127 checks (about 2,300 RPC requests) to 0; the first epoch after a verdict expires still checks everyone, at most once a day. - A format-2
snapshot()is nowv: 2, which 1.0.0 to 1.2.0 refuse (GROUP_INVALID) instead of resuming it as a format-1 group and splitting it; thev: 1format-2 snapshots 1.2.0 wrote still resume. - One identity on two devices is no longer silent:
open()returns a message signed with our identity that this handle did not seal, withotherDevice: true;group.otherDevicereports it; an "already seen" error carriesdata.mayBeOtherDevice. One identity is still one device (TAPI-27 §8). - "no key for epoch N" carries
data.reason(not-yet,expired,snapshot) anddata.retryAfterEpoch; an owner resumed from a snapshot no longer reports "fails authentication" for an epoch whose key it does not hold. - The groups guide says to check
invite.formatbefore joining (a client before 1.2.0 joins a format-2 group without error and then fails on every frame), how to buffer a message that arrives before its epoch, and the owner's cost per epoch.
- The owner no longer checks every member again at each epoch: it reuses its positive verdicts within
— 2026-09-30
Added
- Larger groups (experimental): TAPI-27 §3.8 format 2, up to 128 members in one wire message. Binary roster (104 bytes a member), a marker in the epoch field that every format-1 client refuses (
GROUP_INVALID, tested against the released 1.1.0 code), lazy member checks (open().verified,openVerified,verifyMembers,verifyReuseS, at most 86,400 s, counted from the start of the check),channelKeysVerifier(use it, notapi.groupVerifier(), for format 2), vectorsspec/vectors/tapi-27-group-v2.json. Only positive verdicts are reused; a negative one lasts at most 60 s (VERIFY_NEGATIVE_S), is confirmed by a fresh read and never refuses an epoch. On-demand and background checks share theverifyConcurrencylimit. Format 1 stays the default and its bytes are unchanged (joinGroupwithoutformatignoresverifyMember, as before); one group never mixes formats. TAPI-27 §8 states what format 2 weakens (a verdict reused for up to a day; senders checked on demand) and what it keeps. - A path for AI relays, from nothing to live, with a check at every step.
examples/relay-trial/: a one-command local trial (node examples/relay-trial/trial.mjs): the signing sidecar, a fake upstream and a throwaway identity, then verified calls (OpenAI Chat, streaming, Anthropic), thetapeapi-verifyproxy and a tamper demo. No key, no circuit, no cost.tapeapi-doctor(experimental, new bin in@tapeapi/sdk): checks an AI service in order (name, circuit, container, manifest file and format, delegation and days left,aifield, prices, endpoints, TLS and sidecar readiness, CORS, a verifiable receipt, receipt lookup) and gives each failure a fix in English and Chinese. The receipt check sends a request with an invalid key, which the gateway refuses and the sidecar still signs, so it normally costs nothing (a gateway that accepts any key spends a few input tokens and 1 output token, 16 on OpenAI Responses);--key-envmakes one real, billed call on the operator's own key, sent only to the checked host over https (--allow-httpfor a loopback sidecar) and shown as***in every output, echoes included. Exit status 0 / 1 / 2 / 3 (3: the chain or the network could not be reached, retry) for CI;--json,--strict,--offline, and an address mode to check before publishing.- The AI providers guide opens with "From zero to live": each step, who pays, the command that checks it and what it should print.
- Security hardening, learned from Polkadot's shared-security ideas (experimental; every option off or warn-only by default, 1.0 behaviour unchanged). On
createTapeAPI:pin: every read of one resolution is pinned (EIP-1898blockHash) to one block that nodes ofquorumoperators confirm at the chain's finality tag (finalizedon BNB Smart Chain,safeon an L2); a block older thanmaxPinAgeSis refused as the new client codeRPC_STALE.svc.pinnedrecords the block.rpc.confirmedBlock().sentinel('warn'default,'strict','off'): on the chain's own TapeOut contracts, resolve reads the ERC-1967 implementation slots of the DeWebHub and the SiteRegistry and compares them with the known implementations, and derives the container locally (ERC-6551 CREATE2) to cross-checkaccountOf. It notices an upgrade; it cannot prevent one.'warn'reports insvc.warningsandonWarning(default:console.warnonce);'strict'refuses with the new codeCONTRACT_UNKNOWNorMANIFEST_INVALID. A failed slot read only warns.requireContentSig: refuse a manifest without a valid holder content signature (TAPI-20 §3.10). Without it, a content signature is only reported (svc.contentSig, warnings), and an error while checking it is the warningCONTENT_SIG_UNCHECKED: it never fails a resolve.delegationFloor: remember the highest delegationexpiresseen per chain, container, holder and signer, and refuse an older one put back (a rollback of a re-published manifest); a new signer starts its own floor, andapi.clearDelegationFloor(error.data)forgets one. A site writer can still put back an older holder-signed delegation with a higherexpires.onWarning,clock.
- TAPI-20 §3.10: optional holder-signed manifest content (
contentSig,ManifestContent(address container,bytes32 contentHash)in the delegation domain), so that whoever can write the site cannot silently changeai.baseUrlor prices under a valid delegation when the client requires it.sig.manifestContent*helpers, vectorsspec/vectors/tapi-20-content.json(checked byverify.py). securitynamespace:erc6551Account; ContradictionRecord v1 (contradictionRecord,contradictionsOf,verifyContradiction): evidence anyone can check when providers sign conflicting answers to the same request at the same block (verifyContradictionsaysvalidonly whensignerOfconfirms every signer as the provider's delegated signer, otherwisesignaturesConsistent: true; the block-pinned form suits methods whose result is deterministic);withSpotCheck, a wrapper that re-asks an independent provider at a given rate (0, off, by default).
Changed
- Groups: the TAPI-27 §3.3 step 6 member checks run in parallel (
verifyConcurrency, default 8), with the same outcome and errors as before. A 32-member cold start at 280 ms per request: about 45 s before, about 6 s now. - Every
42.1013.tapeexample says it is an example name (it has no manifest, so a command run as written stops with "no file at /.well-known/tapeapi.json") and points to the local trial;tapeapi-verifyadds a hint totapeapi-doctorand the trial when it cannot use a name. ATTEST_DISAGREEerrors carry the verified signed envelopes and the request inerror.data(envelopes,request), so the disagreement can be kept as evidence.- A cold resolve on the chain's own contracts sends the sentinel's two ERC-1967 slot reads to each node alone, in the same round as the batch (never batched, so the
eth_callbatches stay those of 1.1.0): still 4 rounds, 18 requests instead of 12 on BNB Smart Chain and X Layer (24 on Base, which has four default nodes), cached for 300 s.pinadds one round and 3 requests there (sometimes two rounds on BNB Smart Chain), and two rounds and 8 requests on Base (measured 2026-09-30). - A read pinned to a block (an EIP-1898 object or a hex block number, for
eth_call,eth_getCode,eth_getStorageAt) that a node answers with "header not found", "unknown block" or-32001counts as that node not answering, not as a disagreement: a node that lags behind the pinned block no longer makes the read fail withRPC_DISAGREE. The nodes that have the block must still agree, andquorumoperators must still answer (otherwiseRPC_UNAVAILABLE). This applies torpc.call/rpc.ethCallwith a hex block number too.
[1.1.0] — 2026-09-30
Added
- One-command signing for LiteLLM Proxy (
examples/litellm-sidecar/). A docker-compose package puts the signing sidecar in front of LiteLLM Proxy (the official imageghcr.io/berriai/litellm:v1.103.0, pinned, with PostgreSQL for virtual keys); LiteLLM is unchanged, and users keep their virtual keys and SDKs, pointing only the base URL at the sidecar. The price table'sids are LiteLLM'smodel_names, which LiteLLM reports back inmodel. The entry is the new-api package's with LiteLLM's profile (examples/new-api-sidecar/server.mjsgains an optionalprofile; its defaults are unchanged) and one stream adjustment throughcreateAIProxy'sformatsoption: a Responses stream that ends atdata: [DONE](LiteLLM sends Responses events asdata:lines only) gets its receipt before it.smoke.mjsand the tests run without Docker or LiteLLM;e2e.mjschecks a real LiteLLM with the officialopenaiand@anthropic-ai/sdkpackages in strict mode. A sidecar in front, not a LiteLLM callback: a callback sees parsed objects, not the bytes a receipt proves.
Fixed
- RPC batching recovers from a transient network error (
sdk/src/rpc.js, FIXED P101-3). One reset connection on a JSON-RPC batch made that node takeeth_calls one by one for the client's lifetime. Now only what a node answered about a batch (an HTTP error, a single object, a 200 body that is not JSON) stops batching to it for good; a failure with no answer (the connection reset, the body cut off mid-read) pauses batching to that node until 5 minutes have passed or it has answered 20 calls alone. The calls of the failed batch are still asked again one by one, as before. Performance only: what counts as an answer and every quorum rule are unchanged. - A relay room whose storage write fails no longer keeps the frame (
examples/cloudflare-worker/relay-room.js,examples/relay-service/relay-core.mjs, FIXED P101-4). relaySend answered with the error but the frame stayed in memory, so the client's retry added it a second time. The frame is now pending until its own write lands: no read hands it out and no long-poll wakes for it; if the write fails, relaySend answers with the error and the frame is taken back out (its index, the frame a full ring had shifted out for it, the per-source 0x03 / 0x04 count), so the retry is the only copy, before and after an eviction. The relay core without storage (the Node relay) is unchanged. - The sidecar also strips LiteLLM's injected Chat usage chunk (
sdk/src/ai-openai-chat.js, FIXED P101-b). When the sidecar asked the upstream for usage on the client's behalf, it removed only a usage chunk withchoices: []; LiteLLM sends it withchoices: [{ index: 0, delta: {} }], so the client received a chunk it had not asked for. A usage chunk whose choices carry nothing (an empty delta, nofinish_reason, nologprobs, no other member) is now the injected one too; any content,finish_reasonor other member keeps a chunk in the stream.examples/litellm-sidecarno longer needs its own Chat adjustment; its Responses adjustment (data: [DONE]as a final line) stays. - The monitor reports a signed answer without
resultinstead of crashing (scripts/monitor.mjs, FIXED P101-2).judgeAsyncandcheckRelayAsyncthrew a TypeError on it, and the run could end without a report; it is now a FAILED line that says so.
Fixed (docs and website)
- Docs: every copyable
tapeapi-verify 42.1013.tape/tapeapi-mcp 42.1013.tapecommand (both READMEs, the SDK README, the homepage, the AI providers and MCP guides, the new-api sidecar README) now says that 42.1013.tape is an example name to replace with your service's. The MCP guide'screateMcpEndpointexample uses the public service's name,11.1013.tape. The SDK README and the holder console use1.2.230.tape, which exists on X Layer, instead of1.2.344.tape, which does not. - Channels guide:
node conformance/relay.mjs --urltakes the relay's site root (https://relay.tapeapi.fun), not the/tapeapi/v1address used in an invite'srelays[].url. - Upgrading to 1.0: the error-code table now lists
METHOD_NOT_ALLOWED(a provider's unsigned HTTP 405 for a non-POST request, a transport failure to clients) andNAME_TAKEN(WebMCP, inhandle.skipped[].codewhenregisterToolfails). The Experimental list now namesMAX_CONTRIBUTION_BPS,RECOMMENDED_CONTRIBUTION_BPSandMAINNET.bem, as tagged in the type declarations. - The Chinese "Call a service" guide links to the upgrade guide's error-code table, like the English one.
- The link from the MCP guide to "Go live from a phone" in the provider guide now works on GitHub as well as on the site: the heading reads "Cloudflare and holder console" (no
+), so both produce the same anchor. - Homepage and status page:
<html lang>and the tab title follow the language switch (on first load and on toggle). - The site now serves
/favicon.ico(16/32/48 px, generated fromfavicon.svg) instead of a 404.
Tests
- Three timing-dependent tests no longer fail on a loaded machine (FIXED P101-1).
bus-fuzz, the fallback scenario ofbus-privacyand R8-S1 inreview-archran on the real clock (budgets of 200 ms and 20 s, a 60 ms back-off), so a busy machine or CI runner sometimes failed them. They now run on a virtual clock (sdk/test/helpers/clock.mjs): simulated node latency is the only time that passes, so each asserts what it always did, on the idle machine it was written for; tiny-budget fuzz seeds now replay too. Each also runs once more on a clock going 50 times fast, which failed before the change.
[1.0.0] — 2026-09-29
TapeAPI 1.0.0, the first stable release. Nothing on the wire and nothing in the interfaces of @tapeapi/sdk and @tapeapi/server changed since 1.0.0-rc.5: the specifications change status, not content.
What 1.0 promises
- Semantic versioning from 1.0.0 on. Code written against the 1.0 documentation keeps working in every 1.x release; a breaking change comes only in 2.0. Additions (a new optional option, a new field, a new error code) may come in any minor release.
- Stable: every export of
@tapeapi/sdkand@tapeapi/server, the command-line toolstapeapi-mcpandtapeapi-verify, and the methods and result shapes of the public services (api.tapeapi.fun, relay.tapeapi.fun), except what is marked otherwise. - Experimental (
@experimental; may change in a 1.x minor release, each change in this changelog): everything that pays (TAPI-22 payment channels, the escrow, vouchers,api.payer(),maxPrice, the channel builders ofapi.tx,api.chain.escrow.*), the ServiceDirectory (api.chain.resolve(),api.chain.serviceOf(), directory labels, thedirectoryoption,labelToBytes32and the label helpers ofabi), and the whole@tapeapi/sdk/bus-privacysubpath. Reading prices from a manifest and the codesPAYMENT_REQUIRED,BAD_VOUCHERandPRICE_CHANGEDare Stable: a free service's client can meet them too. - Internal: marked
@internal, or not exported.
The full lists, what changed from 0.x and the error-code table are in the upgrade guide: docs/guides/upgrade-1.0.md (https://tapeapi.fun/docs/en/upgrade-1.0).
Specification statuses (TAPI-1 §4.1)
- TAPI-20, TAPI-21, TAPI-23, TAPI-26 and TAPI-27 are Stable (v1) since 2026-09-29; their
Targetrows are gone. Every field, encoding, signature domain and error code they define keeps its meaning; a revision may add only optional content and non-normative text; a breaking change is a v2 with its own wire markers, and v1 is not withdrawn earlier than 12 months after v2 becomes Stable. - TAPI-20 §3.5 (ServiceDirectory) is Experimental, outside the freeze (TAPI-1 §4.1, freeze rule 3), and so are the places that use it: a label as input to §3.6 step 1, the
serviceOfcross-check andverifyDelegationin §3.4. Resolution by name, container or(circuits, tokenId)needs no directory. - TAPI-21's error codes,
PAYMENT_REQUIREDandBAD_VOUCHERamong them, are frozen with TAPI-21, although the payment flow that uses them is in TAPI-22, which is Experimental. - Unchanged: TAPI-22 and TAPI-25 Experimental, TAPI-24 Withdrawn, TAPI-1 Draft. No normative text changed. The TAP numbers are still proposals to the TapeKit maintainers (TapeKit#8).
- The Chinese halves of TAPI-26 and TAPI-27 gain the RFC 2119 keyword sentence their English halves already had (translation only; both halves now count the same keywords).
Website and READMEs
- The homepage, both READMEs, both package READMEs and the introduction (English and Chinese) say 1.0.0 instead of pre-release; the homepage roadmap marks 1.0 done; the spec index and the READMEs' spec tables show the new statuses. The public-API, MCP and groups guides drop their stale "pre-alpha" label (the no-SLA and no-audit notes stay).
Added
- The monitor checks the relay's asynchronous delivery (RELAY-1 would now alert within one 30-minute run).
scripts/monitor.mjsposts one random 0x03 test frame (64 to 200 bytes) to a random room throughrelaySend, leaves the room idle for 60 s (ASYNC_WAIT_S, or--async-wait-s=N), then reads it back with a brand-new client (relayRecvfrom -1, epoch null). It passes only on exactly that one frame, the same bytes, under the epochrelaySendanswered; the report says why otherwise ("0 frames, epoch null: the relay lost the room while idle"). Network-layer failures are retried twice and reported as DOWN, apart from a relay that answered wrongly (FAILED). The send goes out first and the other checks run during the wait, so a run takes about 65 s; the workflow's timeout is now 15 minutes.relaySendis free on relay.tapeapi.fun and the frame expires with its room after 15 minutes; the monitor still sends no transactions and holds no keys.
Fixed
- The public copy keeps git's executable bit. The script that stages the public copy wrote every file 0644, so the CLIs in
sdk/binreached the public repository without their executable bit once; they keep 100755 now, with a test.
From 1.0.0-rc.1 to 1.0.0-rc.5
- rc.1, the interface freeze:
INVALID_ARGUMENTfor the caller's own mistakes; fixedTapeAPIErrortop-level fields (details ine.data, the old names deprecated until 2.0);service,relayClients/busClients,rpcTimeoutMs,namefor MCP,clockin Unix seconds;openai-proxyremoved; narrower public faces forai,channel,rpcandgroup;createVerifyingFetchverifies streams the official SDKs read; provider dev mode only throughopts.dev; TAPI-1 §4.1 statuses; the TAPI-20 §6.1 mainnet manifest and TAPI-23 §6 vectors (the Python verifier went from 159 to 249 checks). - rc.2:
api.resolve()in 4 round trips and 12 requests instead of 7 and 21; the server could serve one voucher twice (fixed); the homepage's fonts are files. - rc.3: the website's vendored SDK lives in a content-hashed directory, so a release never mixes old and new modules.
- rc.4: the last error-code alignments (a server without
rpcUrls, and WebMCP's caller-state errors, areINVALID_ARGUMENT); loosely written metered paths are refused instead of passed on unverified; review fixes with no interface change. - rc.5: the Cloudflare relay keeps its rooms in Durable Object storage, so idle rooms no longer lose frames.
Upgrading
- From 0.x: follow the upgrade guide.
- From 1.0.0-rc.4 or rc.5: nothing to change. From rc.1 to rc.3: code that matched
INTERNALfor a server withoutrpcUrls, orBAD_REQUESTfor WebMCP'srefresh()and calls after dispose, now seesINVALID_ARGUMENT.
[1.0.0-rc.5] — 2026-09-29
Fixed
- The Cloudflare relay (relay.tapeapi.fun) no longer loses a room when Cloudflare recycles an idle Durable Object. Seen live on 2026-09-29: a frame posted to a room was read back at once, but every read from +30 s to +400 s found 0 frames and
epoch: null, with no redeploy in between.RelayRoomkept its room only in memory, and Cloudflare evicts an idle object within seconds, so a room designed to live 15 minutes lived about ten seconds and asynchronous delivery (group invites, TAPI-26 invites, messages to a peer that was not polling at that moment) mostly failed. The room is now written to the object's SQLite-backed Durable Object storage, one key per frame plus one meta key, and a recreated object restores it with the same epoch and indices, so clients' cursors keep working.relaySendanswers only after its frame is stored; a frame a full ring drops is deleted from storage; the Durable Object alarm clears an expired room and its storage (15 minutes idle, 10 for a handshake-only room, as before), and only then does a new room get a new epoch. The relay's methods, parameters and answers are unchanged, and the Node relay (examples/relay-service) still keeps rooms in memory:createRelayCoregained an optionalonChangehook andrestore(), which it does not use. Privacy: frames (ciphertext), room names, indices and timestamps are now at rest in Cloudflare's Durable Object storage until the room expires; the source of a post (IP address or paying consumer) is still held in memory only and never written. The privacy page says so.
[1.0.0-rc.4] — 2026-09-29
Error codes (the last alignments before 1.0)
@tapeapi/server: a provider built withoutrpcUrlsnow saysINVALID_ARGUMENTon its first chain read, with the fix in the message (rpcUrls: rpcUrlsFor(<chainId>)); it wasINTERNAL.- WebMCP:
refresh()with nothing exposed,refresh()after dispose, and a tool called after dispose areINVALID_ARGUMENT(a mistake in the page's own calls); they wereBAD_REQUEST, which reads like the agent's input. A malformed tool input from the agent staysBAD_REQUEST.
Fixes from the release-candidate review (P2 items, observation period): no interface or return shape changes.
Fixed
- A metered path written loosely is refused, not passed on unverified.
/v1//chat/completions,/v1/chat/%63ompletions,/v1//messagesand/v1/%6Dessagesmatched no format, so the reference sidecar forwarded them with no receipt, andcreateVerifyingFetchandtapeapi-verify --strictlet the answer through unverified and unreported; api.openai.com and api.anthropic.com serve these spellings. A path that matches a format only once percent-encoded unreserved characters are decoded, repeated slashes collapsed and a trailing slash dropped is now: 400bad_pathfrom the sidecar, before anything goes upstream;INVALID_ARGUMENTbefore sending from a strictcreateVerifyingFetch(the samee.dataas an endpoint mismatch), oronReportwithmismatch: trueandnot verified: path mismatch; 400 fromtapeapi-verify --strict, a logged warning without it. Paths no format meters pass through however they are written. (1.0.0-rc.1's "createVerifyingFetchno longer lets anything through unverified" did not hold for these spellings.) createProvider({ dev: true })alone allows a priced method on a zero-address escrow. The manifest also had to saydev: true(1.0.0-rc.1 said itsdevfield switches nothing). What dev allows is exactly a zero-addresspayment.escrow, not a priced manifest with nopaymentfield, which stays invalid; the rc.1 wording "priced methods without an escrow" is corrected in the guide and the declarations.createAIProxyno longer reads the manifest'sdevfield when checking its endpoints (dead code; the refusal was the same).- The refusals of 0.x option names link https://tapeapi.fun/docs/en/upgrade-1.0 (they named a path the npm package does not contain, or nothing); "rpcUrls not configured" says how:
createTapeAPI({ rpcUrls: rpcUrlsFor(<chainId>) }). - The SDK's own code no longer reads
TapeAPIError's deprecated top-level aliases (rpc.js,bus-privacy.js);channel.jskeeps its reads until 2.0. Unused imports and a helper removed fromtapeapi-verifyand the MCP proxy. tapeapi-verifyreads a request path that starts with//as a path, not as a host.examples/web2-adaptertakes dev mode from the environment (dev: !PROD), not from the manifest.
Docs
labelToBytes32and theabilabel helpers are marked@experimental(ServiceDirectory only), as the upgrade guide's list already implied.- groups: a rate-limited delivery carries
rateLimited/retryAfterSon the failed entry ofe.data.deliveries; consume, faq: a script runs wherever@tapeapi/sdkresolves (a clone, or a project with the release package); provide:publishManifestreturns{ txs, key, size, sha256Hash }; sdk/README lists/ai,/mcp,/bus-privacyandtapeapi-verify; both package READMEs say pre-release, not pre-alpha; README test count.
Tests
- The browser smoke test loads "My services", the playground, the receipt checker, the holder console and the status page and fails if one throws while loading (RC-1 could not show there); its TAPI-21 envelope checks sign EIP-191, as the vectors have since 0.5.0.
[1.0.0-rc.3] — 2026-09-29
Website
- The vendored SDK lives in a content-hashed directory, so a release can never mix old and new modules. The 1.0.0-rc.2 fix (
Cache-Control: max-age=0on/playground/vendor/*) did not hold in production: the zone's Browser Cache TTL rewritesCache-Controlon every.jsto 4 hours, so a returning visitor could still get a newindex.jswith a cached oldai.js(a problem since 0.x).npm run build:playgroundnow writes the whole tree tosite/playground/vendor/<hash>/, named by the first 10 hex digits of a SHA-256 over every vendored file and path, and removes the old directory. The files themselves are unchanged, byte for byte whatsdk/srcand@noblehold, and still import each other by relative path, so a module has one URL. The same run regenerates the import maps of the playground, the receipt checker and "My services", the vendor paths in their scripts, and the checker's policy hash. A new test fails unless changing any SDK file changes the directory name. The_headersrule stays as a second line.
[1.0.0-rc.2] — 2026-09-29
SDK
api.resolve()takes 4 round trips instead of 7, and 12 HTTP requests instead of 21 (no interface or return shape changes). Against the default BSC nodes,resolve('11.1013.tape')went from a median 2.77 s to 1.79 s on a new client, and from 2.00 s (6 rounds, 18 requests) to 0.68 s (2 rounds, 6 requests) when the same client resolves it again (10 interleaved runs each). How: the second, identicalaccountOfis gone; reads that do not depend on each other go out together (a name'sisCPUandownerOfwithaccountOf, and for any target steps 3-5 of TAPI-20 §3.6 in one round), each still checked in its old place with the same error codes;fileInfostill comes beforeread.createRpcsends concurrenteth_calls to one node as a JSON-RPC batch of at most 3 (all ten default nodes take one; dRPC's free plan refuses more than 3), each call still its own quorum round; a node that does not take a batch is asked call by call from then on, and a batch refused with an HTTP error is never read as answers.resolvekeepscpuAt(n)andaccountOf(circuits, tokenId)answers (never errors) for at most 300 s, per chain; the holder, the manifest file and its hash are read on every resolve.- Server: one voucher could be served twice. A priced call that committed while the next call with the same cumulative was reading the escrow was missed by both checks. The in-flight reservation is now read before the store, both after the escrow read.
Website
- The vendored SDK is revalidated on every load (correction after release: this did not take effect; Cloudflare's browser-cache setting forces 4 hours on .js files. Fixed at the root in 1.0.0-rc.3). Its modules import each other without a content stamp (so one module is never loaded under two URLs), and Cloudflare cached them for 4 hours, so a returning visitor could run a new
index.jsagainst a cachedai.jsright after a release./playground/vendor/*is now served withCache-Control: public, max-age=0, must-revalidate(an unchanged file costs one 304).
- The homepage's fonts are files, not data URIs. The four subsets (same bytes) moved out of
site/style.cssintosite/fonts/, all withfont-display: swap, and the homepage preloads Archivo. The render-blocking stylesheet drops from 93 KB to 27 KB (58 KB to 6 KB brotli): on a throttled Slow 4G load the first paint comes at about 550 ms instead of 690 ms, and every face is in by about 680 ms. Once the fonts are in, the page is pixel for pixel what it was.scripts/version-assets.mjsnow stamps fonts too (url(...woff2?v=<hash>)in stylesheets and a preload'shref), and the publishing step inlines the fonts back into the self-contained DeWEB copy, which still makes no external request.
[1.0.0-rc.1] — 2026-09-29
Breaking changes toward 1.0
The interface freeze review (1.0 plan G1). Every change below can break 0.x code; what to write instead, what 1.0 promises (Stable, Experimental, Internal) and the full error-code table are in docs/guides/upgrade-1.0.md.
INVALID_ARGUMENT, a new client code for the caller's own mistakes. A configuration or argument error (createRpc,createTapeAPIwithoutrpcUrls, an unsupportedresolvetarget,payeroptions,txbuilder arguments,callQuorumoptions, group-delivery carriers,createProvider/createAIProxy/createMcpProxyoptions) is nowINVALID_ARGUMENT, raised before anything is sent. It used to beRPC_UNAVAILABLE,MANIFEST_INVALID,BAD_VOUCHER,ABI_INVALID,BAD_KEY,CHANNEL_INVALID,BAD_REQUEST,QUORUM_FAILED,METHOD_NOT_FOUNDorGROUP_DELIVERY, so a retry loop onRPC_UNAVAILABLEcould spin on a missingrpcUrls. The codec layers (abi,canon) and the protocol refusals ofcallQuorumare unchanged. TAPI-21 §3.4 lists it, withGROUP_DELIVERY,BAD_RESPONSE,BUS_PRIVACY,BUS_BUDGETand the MCPerror.data.codevalues.TapeAPIErrortop-level fields are fixed:name,code,message,data,signed,httpStatus,cause, andts,block,id,sig,erroron a signed provider error. Every other detail is indata(e.data.tooLarge,e.data.rpcCode,e.data.rpcRevert,e.data.agreed,e.data.failed, ...); the old top-level names still read, as deprecated aliases, until 2.0.channelexports:toHex(bare hex, unlikeabi.toHex),fromHex,toBase64,fromBase64and the test hooks_keySchedule,_busMerge,_busKindOfare no longer public;@tapeapi/sdk/channeland the rootchannelnamespace point at a public face (sdk/src/channel-public.js).signo longer re-exportskeccak256,toHex,bytesToHex,hexToBytes(useabi).@tapeapi/sdk/rpcexportscreateRpcandRPC_BODY_LIMITonly (readJsonBounded,describeUrl,isNodeLimitare internal).abi.FUNCTIONSandabi.SERVICE_TUPLEare@internal.service, notsvc:channel.relayTransport({ service }), relay carriers{ api, service, payer }, the WebMCP handle'sservice.svcis refused with a pointer to the upgrade guide.relayClientsandbusClients, always lists:deliverGroupUpdate({ relayClients: [...], busClients: [...] }),checkGroupInvites({ relayClients: [...] }), clients{ api, service, payer? }and{ address, sendTx }.relaysstays the roster's and the invite's list of relay references{ url, container }(createGroup,resumeGroup,channel.createInvite);relay,bus,relaysandbusesare refused by the delivery functions.- Every
nowis Unix seconds.createGroup,joinGroupandresumeGrouptakeclock, a function returning Unix seconds; the 0.xnow(a function of milliseconds) is refused, and so is a clock returning milliseconds (above 1e11, e.g.Date.now), on the owner's side and the member's:clock must return Unix seconds. The TAPI-27 vectors are unchanged. channelRecordFlooris keyed<chainId>:<container>and shared with the other chains' clients (forChain), so the anti-rollback floor persists on X Layer and Base too; a 0.x entry is migrated on first read.- The
openai-proxysubpath of@tapeapi/serverandcreateOpenAIProxyare removed: use@tapeapi/server/ai-proxyandcreateAIProxy. - MCP endpoint and proxy:
createMcpEndpoint()returns{ handleRequest, tools }(washandle);createMcpEndpointandcreateMcpProxytakename(wasidentity: { name };identityis refused).UPSTREAM_TIMEOUT_MSisAI_UPSTREAM_TIMEOUT_MSinai-proxyandMCP_UPSTREAM_TIMEOUT_MSinmcp-proxy. ai.createVerifyingFetchno longer lets anything through unverified. Streams read by the official SDKs are verified (they stop reading at[DONE]/message_stop, so the old end-of-stream check never ran). A stream ends at its final event, at[DONE]or when the upstream closes, whichever comes first; in strict mode the chunk in which it ends is released only once a receipt that came before the end verifies, else the iterator throwsRECEIPT_INVALID(a[DONE]inserted beforeresponse.completedused to let an unverified Responses stream through). Nothing waits for the upstream to close the connection, and withstrict: falsenothing is held at all; an upstream that breaks off after the end does not fail the call. A custom format that streams must name its final event (stream.final): strict refuses one without it withINVALID_ARGUMENTwhen the fetch is made, otherwiseonReportwarns once. A metered request to a host other than the manifest's endpoint (localhostfor127.0.0.1) is refused withINVALID_ARGUMENTin strict mode, reported withmismatch: trueotherwise.tapeapi-verify --strictlikewise ends the stream in an error event when no receipt before its end verifies, and passes on only whole events. In strict mode a whole answer whose receipt fails is no longer thrown: it becomes an HTTP 502 in the API's error shape (codeRECEIPT_INVALID) withx-should-retry: falseandx-tapeapi-verify-error: RECEIPT_INVALID, so the official SDKs throw anAPIErrorand do not retry it (a thrown error was wrapped and retried twice by default: three requests, each possibly paid); a caller of the fetch itself checksres.ok.onReportfires as before. Regression tests drive the officialopenaiand@anthropic-ai/sdkpackages (root devDependencies only).- Provider
devisopts.devonly:createProviderrelaxes its payment checks (priced methods without an escrow, the in-memory meter warning) only fordev: true;allowHttponly allows http endpoints; a manifest's owndevfield switches nothing, increateProviderandcreateMcpProxy. rpcTimeoutMs:createTapeAPI, itschains[id]andcreateProvidertakerpcTimeoutMsfor the timeout of one RPC request;timeoutMsthere is refused (api.callkeeps its per-calltimeoutMs).- Decimal strings out:
api.chain.tokenOf()returnstokenIdas a decimal string, like everytokenIdandprocessorthe SDK returns. The public service'stapeNameis unchanged. - A narrower frozen surface: the root
ainamespace and@tapeapi/sdk/aiare a public face (sdk/src/ai-public.js): the helpers the reference sidecar and the website use stay, marked@internal;amountOf,pricesOf,sseDigestOfPayloads,sentinelOf,rootOf,saltRequestBodyand the limit constants are no longer exported there.group.senderKeyandgroup.buildEpochleave the publicgroupnamespace. The wholebus-privacysubpath (options, defaults,stats().privacy) is@experimental. - Option interfaces have no index signature:
CreateProviderOptions,ChannelSelf,ChannelPeerand WebMCP'spaidreject unknown keys at compile time. - Types:
GroupisGroupHandle | OwnerGroup(withGroupSnapshot,Roster);ManifestBaseis shared by the two proxies; payment declarations carry@experimental.
Also: api.addresses.factory; PROVIDER_UNAVAILABLE carries data.timedOut / data.aborted for the caller's own timeout or abort; the CLI help lists tapeapi-mcp --dev and the exit status (0, 1, 2; no environment variables).
Website and READMEs
- Homepage rewritten for the current positioning (
site/index.html,site/style.css; 1.0 plan G9). It leads with the signed receipt layer for AI services (the sidecar, what a receipt proves and what it does not: which model ran), then MCP, private channels and groups, and multi-chain. New sections: a start-here path per reader (AI providers, MCP server authors, app developers, circuit holders), the receipt layer with a figure and a "what a receipt proves" table, chains (BNB Chain with payments only there; X Layer and Base read-only), and privacy (what is protected and what is not). "Live now" adds the AI sidecar and new-api package (available, self-hosted only: it handles API keys),tapeapi-verify, the console's AI price tables, one-call group delivery, the spot-check probe and the receipt checker. Paid channels and their gas and amortisation figures move to "Experimental: paid channels" (not deployed); the fee note uses the current wording, and says no call is charged today. Spec statuses follow TAPI-1 §4.1. The examples userpcUrlsFor(56); a new one plugscreateVerifyingFetchinto the official OpenAI SDK. Every example was run against the v0.8.0 release package (the AI one against the reference sidecar inexamples/ai-proxy, since no outside provider has published a price table yet). - README.md and README.zh-CN.md rewritten to the same standard: the value in the first lines, a table of first steps per reader, short examples that run (install from the release package, AI receipts,
tapeapi-verify, MCP), a Mermaid diagram of the layers, what a receipt proves, status and commitments, privacy, fees and the specs with their statuses. Contract addresses and repository detail now live in the guides.
Specification statuses toward 1.0 (TAPI-1 §4.1)
- TAPI-1 §4.1 defines the interim statuses this repository uses until TapeKit adopts a numbered-proposal process: Draft, Stable (v1), Experimental and Withdrawn. Stable (v1) freezes every field, encoding, signature domain and error code; a revision may only add optional content or non-normative text; a breaking change is a new version (v2) with its own wire markers, v1 stays valid beside it and is not withdrawn earlier than 12 months after v2 becomes Stable. When TapeKit assigns numbers or statuses, TapeKit's prevails and the front matter records the mapping. A new optional front-matter row,
Target, names the status a Draft intends to reach. - Status rows. TAPI-20, TAPI-21, TAPI-23, TAPI-26 and TAPI-27 stay Draft with
Target | Stable (v1) at TapeAPI 1.0; TAPI-22 (metered payment and escrow) and TAPI-25 (circuit-verified methods) are Experimental; TAPI-24 (intent RFQ, frozen since 2026-09-21) is Withdrawn. No normative text of TAPI-20 to TAPI-27 changed.
Test vectors
- TAPI-20 §6.1 mainnet manifest filled in: the live manifest of
11.1013.tape, read only throughresolveon the SDK's default BSC nodes (quorum 2 by operator, all three operators identical), everyeth_callpinned to block 124552456 by EIP-1898{ blockHash }: size 3414, SHA-2560xee57f304…3b52c37a, signer, delegation expiry and holder. Recorderscripts/record-mainnet-manifest.mjs, fixturesdk/test/fixtures/mainnet-11-1013-manifest.json, offline replaysdk/test/mainnet-manifest.test.mjs(clock pinned to the block; the table in the spec must quote the fixture). The delegation is renewed before 2026-12-10, so the fixture pins a block's state, not the current manifest. - TAPI-23 §6 vectors:
spec/vectors/tapi-23-attested.json(fromscripts/gen-vectors.mjs), two providers signing with public test keys, real Ethereum values for the example request (block 20000000, USDTtotalSupply()), two agreeing cases and threeATTEST_DISAGREEcounterexamples, replayed throughcallQuorumbysdk/test/attested-vectors.test.mjs. §6 now says plainly that no live service offersattestedReadyet (11.1013.tapedoes not) and what the smallest change would be. spec/vectors/verify.pychecks both independently: it re-encodes the mainnet calldata, ABI-decodes the answers, hashes and parses the manifest and recovers the delegation toownerOf; and it rebuilds each TAPI-23 envelope digest, recovers both signers and decides agreement from §3.4 alone. 159 checks before, 249 now.
[0.8.0] — 2026-09-28
ChannelBus read privacy: contract-wide reads by default, cover rooms as the fallback
busPrivacy.busPrivacyReader(new modulesdk/src/bus-privacy.js, subpath@tapeapi/sdk/bus-privacy): the options and the reader ofchannel.busReader, plusmode,cover,contract,setMode(),coversandstats().privacy. It wraps the rpc client busReader reads through, so busReader's own reading code (ranges, union, confirmations, overlap, hold never skip) runs unchanged;sdk/src/channel.jsis not touched.mode: 'contract'(the default): no room topic at all (topics: [Wire]); every frame on the bus is downloaded and filtered locally, so a node learns only that the IP reads ChannelBus. It needs no cover pool, so the first poll waits only for its ownlookbackread. Bounded per poll (contract.maxBytes8 MiB,maxLogs10,000, over every node's answer); over it,contract.onExceed: 'cover'(the default) falls back to cover rooms and warns, and'error'stops the reader withBUS_BUDGET(frames already read are handed over either way). Default because the mainnet ChannelBus carried one log in 500,000 blocks on 2026-09-28: no rooms to draw covers from, and a contract-wide read cost almost nothing.- Coming back, with hysteresis. After a fallback the reader returns to
'contract'by itself once it has spentcontract.retryMsin'cover'(30 min, doubled after each relapse soon after a return, at most 24 h) and a pool refresh made after the fallback shows a poll's traffic at or under half the budget;retryMs: nullnever returns. Covers are drawn once and kept, so the switching shows no new room sets; covers drawn at a fallback come first from rooms seen in quiet contract polls, not from the junk that caused it.stats().privacy.fallbackreports it. mode: 'cover': each of your rooms goes out amongkrooms (default 8), the others real rooms seen in the bus's last 40,000 blocks of logs (read with queries that name no room), in quiet contract polls, or incover.pool, in a fresh random order on every request, catch-up reads of rooms added later included. Cover rooms' logs are dropped before the scanner parses them: never decoded, decrypted or kept. Covers are drawn once per room and kept for the reader's life, and across restarts withcover.store, because changing them lets a node intersect requests. Too few covers is never passed off as cover: the reader warns once per change (effectiveK,short), or throwsBUS_PRIVACYbefore sending anything withcover.onShort: 'error'. Its pool scan costs one request per 5,000 blocks (3-6 s each on 48 Club), about 25-50 s before a fresh process's first read;cover.scanBlockslowers it.mode: 'plain': busReader as before.scanCoverPoolandplausibleRoomare exported too. Tests:sdk/test/bus-privacy.test.mjs.
- No existing default changes. Nothing in the SDK builds a
busReaderfor you (checkGroupInvitesreads relays), so no existing call changes behaviour;busReaderandbusTransportstill name your rooms. The guides now point bus readers tobusPrivacyReader;mode: 'plain'gives busReader's behaviour through it. - Docs: "Read privacy" in
docs/guides/channels.mdand the Chinese guide (the default and why, the first read, falling back and coming back, whatkmeans, what it helps against and what it does not). TAPI-26 §8 gains a non-normative note, "Node correlation", in both languages; no MUST/SHOULD/MAY changed.
Holder console: AI price tables
- The console publishes a manifest with an
aifield (TAPI-20 §3.9).site/console/lib.jsgainsaiProblemsandnormalizeAI, a line-for-line port of the SDK'svalidateAIField(the page loads no library): the same verdict, the same first message and the same normalised bytes, checked insdk/test/console-ai.test.mjson every valid and invalid sample of the SDK tests, the receipt vectors' table, the spec's example,models.example.jsonand 4,000 random mutations of them.manifestProblemsandexpectedManifestacceptaiin its normalised form (what the sidecar serves); anything else new is still refused. - The price table before the wallet asks. Step 5 shows every API format's address and every model's id, aliases and per-currency input, output, cache read, cache write, 1-hour cache write and reasoning prices per 1M tokens, with the spec's defaults marked (
aiPriceTable). Hints, never refusals: a price of 0, a price above a per-currency threshold (AI_HUGE), output below input, a cache read above input, endpoints clients ignore,USDfor display only, a table near the one-transaction limit. Bilingual, with the page's language switch. - Import. Paste or upload a
models.json(the new-api sidecar's bare array, converted bymodelsToAIFieldon the step-4 service URL exactly as the sidecar builds its endpoints), anaifield, or a whole manifest (aiFieldOf). A previewed table must equal the served one (aiDiff) or nothing is published. - Read back after publishing. Once the
putFiletransaction is mined, the page reads the manifest back from the chain (length and SHA-256 against the SiteRegistry) and compares it with the bytes sent, price table included (readBackProblems); a lagging node gets a few tries and a mismatch says to check again later. - Multi-chain. On X Layer and Base the page states that payments are not open and the prices are for display only.
- Docs: "Publish the price table with the console" in
docs/guides/ai-providers.md(and the Chinese guide); the "console cannot publishai" notes there and inexamples/new-api-sidecar/README.mdare gone.
Fixed
- A page's own modules are content-stamped too. Cloudflare caches
.jsfor 4 hours; pages stamped the scripts they load (console.js?v=…) but not what those scripts import (./lib.js), so after a release a returning visitor could run the newconsole.jsagainst a stalelib.jsand the page would fail to load.scripts/version-assets.mjsnow stamps every relative import insite/modules, leaves first, so any change below a page changes the page's own reference; vendored code is left alone so one module is never loaded under two URLs.
[0.7.0] — 2026-09-28
Privacy hardening
- Guessable receipt ids. The AI signing sidecar keeps the upstream's answer id (TAPI-21 §3.5), and some upstreams' ids can be guessed: Ollama's OpenAI-compatible API numbers chat ids
chatcmpl-0tochatcmpl-998, so anyone could walk through the freereceiptmethod and read every receipt kept. The sidecar now estimates the randomness of the ids it sees (idEntropyBits, thresholdID_ENTROPY_MIN_BITS= 64) and warns once in its log when they look guessable (a low estimate, or one id seen twice while kept); keeps receipts per (id,requestSha256), so answers that share an id no longer overwrite each other (an id-only lookup still serves the later one); gives thereceiptmethod a budget of its own (receiptRateLimit, default 10 lookups per client IP per minute, refused with the unsigned 429 of TAPI-21 §3.4); states the configurable lifetime (receiptTtlMs) in the method description (unchanged at the default); and, withrequireRequestHash(off by default), answers only lookups that also namerequestSha256. Worker variablesRECEIPT_TTL_S,RECEIPT_LOOKUPS_PER_MIN,RECEIPT_REQUIRE_HASH; the new-api sidecar takesRECEIPT_TTL_SandRECEIPT_REQUIRE_HASH. TAPI-21 §3.5 (both languages): thereceiptmethod MAY take an optionalrequestSha256parameter that picks the receipt of that request among answers sharing an id, and a provider whose upstream's ids can be guessed MAY refuse a lookup without it; the manifest entry staysparams: { id: "string" }. §8 describes the risk. The receipt shape and the frozen vectors are unchanged. - Request salt.
createVerifyingFetch(new optionsalt, default on) andtapeapi-verify(--no-saltto turn it off) append 64 random JSON whitespace characters (128 random bits) after the JSON text of each request body on a receipt path, so the receipt'srequestSha256can no longer be confirmed by hashing guessed short prompts. The parsed request is unchanged: no field is added or changed (userandmetadata.user_idstay as written, since gateways route caches by them), no token is added, and prompt caches keyed on the parsed prompt are unaffected. Compressed bodies (Content-Encodingother than identity) and non-JSON bodies are sent as they are; an explicitContent-Lengthis dropped when the body grows; the receipt is checked over the bytes actually sent; reports carrysalted. New exportsai.saltRequestBodyandai.SALT_LENGTH. TAPI-21 §8 and the AI provider guide explain why the cache is unaffected. Acceptance by the live OpenAI and Anthropic APIs is not yet measured (the tests use the reference sidecar). - Hash-only MCP receipts and verify links.
verifyLinkused to put the whole receipt, params and result in clear, into the link, so sharing a link shared the call. It now carries the hash-only form by default (mcp.hashReceipt, receiptv: 2:paramsandresult/errorreplaced byrequestHash= keccak256(canonicalJSON({ method, params })) andbodyHash= keccak256(canonicalJSON(result or error)), the two hashes the TAPI-21 digest is built from, so the signature still verifies). The whole receipt goes into the link only when asked:verifyLink(r, base, { content: true }),toolResultOf({ linkContent: true }),linkContentoncreateMcpEndpointandcreateMcpProxy(LINK_CONTENT=1in the MCP proxy Worker),tapeapi-mcp --link-content. The provenance note says which form its link carries. The receipt in_metastays whole. Newsig.responseRequestHash,sig.responseBodyHash,sig.responseDigestFromHashes,sig.recoverResponseSignerFromHashes(the same digest, checked against every published TAPI-21 envelope vector). The verification page reads both forms, shows the hashes and says that low-entropy params (an address, a token id, a price pair) can still be guessed from their hash. The reputation design now attaches hash-only receipts. The TAPI-21 envelope and digest are unchanged. - Honest labels. The verification page has a "What a receipt does not prove" list in both languages: a receipt does not prove which model or program actually ran; a link with content (MCP receipt v 1) contains the conversation; metadata (the service sees the request and IP, public BSC nodes see the reader's IP and which service is checked) is not hidden. A receipt with content is shown with a note saying so, and the footer says the page can be self-hosted. The help of
tapeapi-verifyandtapeapi-mcpand the SDK README say the same. A new test (scripts/privacy-copy.test.mjs) keeps this copy free of the words the plan rules out. - Log minimisation. All seven
wrangler*.toml(the two public services api.tapeapi.fun and relay.tapeapi.fun, the four templates and one more) turn Cloudflare's per-request invocation logs and traces off and redact query strings ([observability.logs] invocation_logs = false,[observability.traces] enabled = false,redact_query_string = true, keys checked against the pinned wrangler 4.141.0); the Workers' own console output stays (on api.tapeapi.fun one line per MCP message with an HMAC caller tag, never the IP). Takes effect on the next deploy. Whether Cloudflare's own request records include the client IP is to be verified. - Privacy page (draft).
site/privacy/(Chinese and English) says what the services we run record and what they do not, what Cloudflare, service providers, public RPC nodes and the chain see, and what is still to be verified (client IP in Workers logs, Pages access logs, our plan's retention). Not in the sitemap, not linked,noindex, and not added to any publishing list.scripts/privacy-logs.test.mjschecks the configuration and the page;scripts/privacy-words.mjsholds the ruled-out words both privacy tests use. - Session-header switch. The AI signing sidecar takes
forwardSessionHeaders(defaulttrue: unchanged behaviour);false(FORWARD_SESSION_HEADERS=0in the Worker and the new-api sidecar) leaves out the clients' session headers (ai.SESSION_HEADERS:x-claude-code-session-id,session-id,thread-id), which let the upstream tie a caller's requests into one session.tapeapi-verify --strip-session-headersdoes the same on the client side. TAPI-21 §8 (both languages, no new keywords) and the AI provider guide say so. Newai.isSessionHeader.
Group delivery (TAPI-27)
- One call delivers a group update. A group uses two kinds of room: the epoch message goes to the group room, and each new member's invite to that member's own inbox room. An application that posted only
epochWireset up a group nobody could join, and the relay rightly returned 0 frames to the members. New in@tapeapi/sdk:deliverGroupUpdate({ group, update, relay, bus })posts the invites to every new (or named) member's inbox room and then the epoch message to the group room, over relays (relaySend, optionalpayer) and / or ChannelBus (the caller'ssendTx), and returns every post with its room and the relay's{ i, epoch }or its error; any failure throwsGROUP_DELIVERYafter all posts were tried (or returnsok: falsewiththrowOnError: false), and a relay's per-source limit is markedrateLimitedwithretryAfterS.checkGroupInvites({ self, identity, relay, cursors })reads the member's inbox room with one cursor per relay and room that keeps the relay's room epoch (first readafter: -1, epoch: null; a stored cursor without an epoch starts over), opens the group invites, and counts what it skips;holderandcheckSelfrefuse a wallet address or a wrong chainId given for the container. createGroup,addMembers,removeMembers,rotateandresumeGroupnow also returnepochandadded(the members that need an invite); the owner's handle hasepochWire, the latest epoch message, for reposts. Nothing existing changed shape. The JSDoc and types ofcreateGroup,addMembersandinviteForsay which room each part goes to.- TAPI-27 §3.5 (both languages): a non-normative note on the two rooms and on cursors. No requirement changed.
- New guide Group chat (
docs/guides/groups.md,docs/guides/zh-CN/groups.md): prerequisites, the owner and member flows, relay or ChannelBus, saving state, a troubleshooting checklist and the limits. New exampleexamples/group-chat/: two throwaway identities over the public relay, with an end-to-end test against a local verified relay.
[0.6.0] — 2026-09-28
Added
- X Layer and Base, read-only. TapeAPI follows TapeOut to X Layer (chainId 196, area code 2) and Base (chainId 8453, area code 3).
@tapeapi/sdk/chainsholds the chain registry (processor factory, DeWebHub, SiteRegistry, binding, ERC-6551 registry and account implementation per chain, each read on chain; the recorded answers are insdk/test/fixtures/chains-onchain.json). Names follow TapeKitkernel/src/name.js:<#ID>.<processor>.tapeon BNB Chain,<#ID>.<area>.<processor>.tapeelsewhere (1.2.230.tapeis #1 of processor 230 on X Layer); new exportsparseTapeName,formatTapeName,isNameShaped,CHAINS,chainById,chainByArea,chainByKey. resolve()takes names on any supported chain,{ chainId, circuits, tokenId }and{ chainId, container }, and every resolved service carries itschainId.createTapeAPI({ chains })configures nodes per chain;api.forChain(id)andapi.chainOfContainer(address)are new. Paid methods of an X Layer or Base service are refused withPAYMENT_REQUIRED("payments are on BNB Smart Chain only"): payments stay on BNB Chain for now.- Default nodes per chain, measured 2026-09-28 and counted by operator: X Layer = OKX (two domains) + dRPC, only two independent operators, so 2-of-2 with no spare (the client warns once); Base = Coinbase, Allnodes, dRPC, Tenderly.
tapeapi-mcpandtapeapi-verifyaccept area-coded names (--rpc-xlayer,--rpc-base); the verification page, playground, "my services" and the console pick the chain from the name (the console has a chain selector and switches the wallet; ChannelBus stays on BNB Chain).examples/new-api-sidecar/: onedocker compose upputs the AI signing sidecar in front of an unchanged new-api relay (smoke test and tests included). Guide for AI providers:docs/guides/ai-providers.md(and Chinese).examples/spot-check/: a versioned probe set anyone can run against an AI service; it verifies each receipt and writes raw data only (reportedprompt_tokensnext to local tokenizer counts), no scores and no rankings.
Changed
- TAPI-20 multi-chain text (§3.1, §3.2, §3.4, §3.6 step 1 and step 3, §5, §6.2): a service lives on the chain of its circuit; the container, manifest,
isCPUand delegation are all read on that chain; the delegation domain is (that chain's chainId, that chain's DeWebHub). The DeWebHub has one address on all three chains, so the chainId alone separates the domains and a delegation for one chain MUST NOT be accepted on another. Identity does not carry across chains. §6.2 adds the digests for chainId 196 and 8453, computed by the SDK and byverify.pyindependently. - Behaviour change: a dotted all-digit string such as
1.2.3is now an X Layer name, not a directory label. A name with a reserved or unassigned area code is refused. BNB Chain behaviour is otherwise unchanged; receipts are unchanged (the chain follows from the service name, and a wrong chain derives a different container, which fails the check).
[0.5.0] — 2026-09-28
Changed
- Fee policy (before anything is charged): no mandatory protocol fee. When paid calls settle through the next escrow version (not deployed; only after an independent audit), a default 1% maintenance contribution comes out of the provider's share (the consumer's price does not change); any provider can set it to 0, or up to 50%. The default is a contract constant: the operator has no fee switch and cannot pause. Planned settlement tokens: BEM (primary), BNB, USDT, USDC, ETH, USD1. The permanent 10% of any TapeAPI revenue to @Theairresearch includes this contribution. TAPI-22 §3.4, docs/FEES.md, README, the site and the guides say so; the escrow code in the repository still starts at 0 and changes with the next escrow version.
Security
- RPC quorum counts operators, not URLs. The default BNB Chain nodes were three dataseeds that all belong to one operator (NodeReal), so "2 of 3 agree" was one operator agreeing with itself.
createRpcnow counts distinct operators (operatorOf) and refuses a configuration with fewer operators than its quorum; the defaults (RPC_DEFAULTS,rpcUrlsFor(56)) are NodeReal, Alchemy and 48 Club, each checked to serve block-hash-pinned reads.
Added
- AI signing sidecar
@tapeapi/server/ai-proxy(createAIProxy): put it in front of any OpenAI- or Anthropic-compatible API and every call gets a signed usage receipt (service, model, tokens, price, request and response hashes), with the bytes passed through unchanged and no change for clients. Formats: OpenAI Chat Completions, OpenAI Responses, Anthropic Messages, OpenAI Embeddings; streams keep the receipt in an SSE comment the official SDKs ignore, placed before the final event. Example:examples/ai-proxy/. @tapeapi/sdk/ai:verifyUsageReceipt,createVerifyingFetch(plug into the official OpenAI or Anthropic SDK), receipt readers and the price arithmetic (exact, BigInt). The manifest'saifield lists the endpoints and the price table (BEM, BNB, USDT, USDC, ETH or USD).- The verification page reads AI usage receipts.
- Frozen specification of the AI layer: TAPI-20 §3.9 (the
aimanifest field: endpoints per format, models with aliases and a price per currency, usage and the amount formula) and TAPI-21 §3.5 (AI usage receipts: envelope, request and stream hashes, delivery, retrieval, client checks, security). Test vectors insdk/test/fixtures/ai-receipt-vectors.json, checked three ways (the sidecar, the SDK andspec/vectors/verify.py). tapeapi-verify, a local verifying proxy in the SDK release tarball: point Claude Code (ANTHROPIC_BASE_URL) or Codex (OPENAI_BASE_URL) at it and every answer's receipt is checked against the service's on-chain manifest;--strictrefuses an answer whose receipt fails. Tested end to end with the real Claude Code and Codex CLIs.- The sidecar forwards the coding agents' session headers (so relays keep session affinity and caching), accepts requests up to 32 MiB, waits up to 600 s for a whole answer and ends a stream silent for 300 s; its own errors carry
x-tapeapi-sidecar-error: 1.
Fixed
spec/vectors/tapi-21-envelope.jsonwas signed over the raw digest instead of the EIP-191 message TAPI-21 §3.3 names; regenerated, andspec/vectors/verify.pynow recovers every vector's signer.
[0.4.0] — 2026-09-28
Added
- Tape out your MCP server.
@tapeapi/server/mcp-proxy(createMcpProxy) puts a signing proxy in front of an existing MCP server (Streamable HTTP, JSON or SSE): the server keeps its own host and domain, gains an on-chain identity (a TapeOut circuit's container), its tool definitions are pinned by digest in the on-chain manifest, and every tool result is a signed TAPI-21 envelope with a receipt. A runnable example for Node and Cloudflare Workers is inexamples/mcp-proxy/. - TAPI-20 §3.8, the optional
mcpbinding:{ endpoint, toolsSha256 }, wheretoolsSha256is the SHA-256 of the RFC 8785 canonical JSON of the tools' model-facing fields (sdk mcp.toolsDigest). Clients compare it with thetools/listthey receive and refuse a difference. - TAPI-21: provider code
TOOLS_CHANGED(HTTP 409): a service whose upstream tools no longer match itstoolsSha256refuses every call, signed, until the holder republishes. tapeapi-mcpchecksmcp.toolsSha256against the service's livetools/list, shows the upstream tools exactly as pinned, returns upstream content with a verified receipt, and pins the tool set (a republished set is a change the user must accept).- The holder console publishes MCP-bound manifests: it reads
tools/listitself, recomputes the digest in the browser, shows every pinned field of every tool before the wallet signs, and refuses invisible or format characters. - The MCP guide covers taping out your own server.
Security
- Adversarial review of the MCP code (MCP-R1 to R7), all fixed with tests: the remote
/mcpcaps chunked bodies and counts only messages it handles; caller tags in logs are keyed hashes (HMAC) rather than plain hashes of the IP; the proxy serves only the digest-covered fields of each tool; tool text that imitates a TapeAPI provenance line is labelled as the tool's own output and the genuine line comes first.
[0.3.0] — 2026-09-28
Added
- Remote MCP endpoint
https://api.tapeapi.fun/mcp(MCP Streamable HTTP, stateless): the eight public methods as MCP tools for Claude, Cursor and any MCP client, added by URL with nothing to install. Every tool result is the service's signed TAPI-21 envelope and carries a receipt (_meta["fun.tapeapi/receipt"]) and a verification link. - Local MCP command
tapeapi-mcpin the SDK release tarball (npx -y --package=https://github.com/BruceLanLan/tapeapi/releases/download/v0.3.0/tapeapi-sdk-0.3.0.tgz tapeapi-mcp 11.1013.tape): resolves any TapeOut service on chain, verifies every answer before the model sees it (a tampered answer is discarded), and pins each service's tool definitions and signer, refusing calls after they change on chain until the user accepts (--allow-changed). - Receipt verification page https://tapeapi.fun/verify/: open a receipt link or paste a receipt; the page resolves the service on chain in the browser, recovers the signer and says Valid, Invalid, Cannot confirm or Not checked, with every check listed.
@tapeapi/sdk/mcp: a transport-free MCP server core (createMcpServer), receipts (receiptOf,verifyLink) andtoolResultOf;@tapeapi/server/mcp:createMcpEndpoint({ provider, manifest })turns any provider into a remote MCP server under the provider's own rate limits.- Docs: an MCP guide (English and Chinese); the homepage, README and roadmap list the MCP endpoint.
Changed
- A signed refusal thrown by
api.callnow carries itsid,siganderror, so it can be receipted like an answer. - The relay template (
examples/cloudflare-worker/wrangler-relay.toml) no longer names the project's own Worker or domain: a fork deploys it asmy-tapeapi-relaywithnpm run deploy:my-relay. The project's own relay config moved toexamples/public-api/wrangler-relay.toml;npm run deploy:relaystill deploys it, unchanged. - Web2 adapter: a
${NAME}in the config expands only from theenvpassed tobuildMethods(noprocess.envdefault; a name missing from it is refused when the service is created), and a hosted policy (HOSTED_POLICY) for running other people's configs: https on 443 only, no IP literals, local or platform hosts, at most 3 hosts, no redirects, a header allow-list, a fixed User-Agent, a streamed 256 KiB body cap and an optional resolver check against private addresses. Responses are capped at 1 MiB under the default policy too. - Site: stylesheets and scripts carry a content hash (
npm run build:assets), and the logo has a fixed size without its stylesheet.
[0.2.0] — 2026-09-27
Added
- ChannelBus deployed on BNB Chain at
0x486110c35d9b90a9d6D85c8063A065f9e7b6b707(code byte-for-byte the tested build);MAINNET.channelBusin the SDK. A test message was read back through public nodes, and the deployment is recorded for a replay test. - Public service https://api.tapeapi.fun (
11.1013.tape): eight free, signed, block-pinned methods:blockNumber,balance,tokenInfo,tokenBalance,nftOwner,pairPrice,bnbUsd,tapeName(sourceexamples/public-api). - Public relay https://relay.tapeapi.fun (
12.1013.tape) for TAPI-26 channels. - Playground https://tapeapi.fun/playground/: resolve any service by name, see every check, call it, copy the code.
- Status page https://tapeapi.fun/status/ and a monitor (
npm run monitor: a GitHub workflow scheduled about every 30 minutes, which GitHub may delay, that opens an issue when a service is down or a delegation has fewer than 14 days left). - Docs site https://tapeapi.fun/docs/ (English and Chinese), generated from
docs/guideswithnpm run build:docs, including a Public API guide. - SDK:
api.resolve('11.1013.tape')resolves TapeOut names (<#ID>.<processor>.tape) throughfactory.cpuAt. - Holder console: publishes a service's own free method list; bilingual; hardened (CSP, anti-phishing notice).
- CI runs the JavaScript, Python and Foundry suites on every push.
- My services https://tapeapi.fun/dashboard/: a read-only dashboard (add service names; health, delegation expiry, links to try or renew; never signs or sends).
- Homepage: live services, a 30-second quick start and a roadmap;
docs/ROADMAP.md,CODE_OF_CONDUCT.md, a rewrittenCONTRIBUTING.mdand current issue templates; GitHub private vulnerability reporting enabled. - Website: social preview (Open Graph / Twitter tags and
og.png), a favicon, a 404 page,robots.txtandsitemap.xml. - The dashboard's "Renew in console" link prefills the console's processor and circuit numbers and service URL; the console only fills those fields after checking them, and never reads, signs or sends by itself.
Changed
- Website: new logo and favicon, one header across all pages, redesigned homepage and social preview.
callQuorum: a signed revert (TAPI-23 §3.3) counts as disagreement and is never an accepted result; other signed errors stay neutral. TapeOut names must be in canonical form (TapeKit SPEC §2.2): lowercase, no leading zeros.- The SDK is installable from each GitHub release:
npm install https://github.com/BruceLanLan/tapeapi/releases/download/v0.2.0/tapeapi-sdk-0.2.0.tgz. - Default RPC nodes are bnbchain, defibit and ninicoin dataseeds (publicnode timed out on every request on 2026-09-27), with a 3 s per-node timeout on the Workers.
- The provider template (
examples/cloudflare-worker/wrangler.toml) no longer names the project's own Worker or domain. - The
examples/web2-adapterconfig calls Coinbase's public price API (spotPrice,cryptoRates) instead of worldtimeapi.org, which no longer answered, and frankfurter at its currentapi.frankfurter.devaddress.
[0.1.0] — 2026-09-26
First public release. Pre-alpha: the free tier runs on TapeOut's deployed contracts; our own contracts have no third-party audit.
Protocol (proposed TAPs, bilingual, English authoritative)
- TAPI-20 service identity: a service is a circuit, its container is the address, the manifest lives at
.well-known/tapeapi.jsonin the container's site, and the holder authorises a signing key with an EIP-712 delegation checked against the current holder. - TAPI-21 signed response envelope with the
TAPI-1/resp/v2digest, bound to the container, the request id, the method and parameters, the outcome and the timestamp; canonical JSON; stable error codes. - TAPI-22 metered payment: cumulative vouchers, per-provider escrow channels, session keys, a withdrawal cooldown, zero protocol fee and an optional voluntary contribution.
- TAPI-23 attested reads of other chains, TAPI-24 intent RFQ (frozen), TAPI-25 circuit-verified methods.
- TAPI-26 private channels between containers (mutual authentication, forward secrecy, ChaCha20-Poly1305) over relays or ChannelBus; TAPI-27 private groups of up to 32 containers.
SDK (@tapeapi/sdk)
- Resolution from a container or a circuit, with manifest integrity, delegation and processor checks through a quorum of RPC nodes that must agree.
callwith envelope verification and price consent;callQuorumacross independent providers pinned to one block.- Vouchers signed by a wallet or a session key, with meter resynchronisation.
- Channels and groups; relay, ChannelBus and fan-in transports.
- A ChannelBus reader that holds rather than skips: it copes with public nodes' history limits, result caps, empty or lagging answers and failures, reads over-full blocks from receipts, and reports every block it cannot read.
exposeTapeAPI: WebMCP tools for in-browser AI agents, with per-call and total budgets for paid methods.
Provider runtime (@tapeapi/server)
createProviderfor Node (listen) and the Fetch API (handleRequest, Cloudflare Workers).- Signed answers and errors, voucher checks, per-consumer metering (shared D1 meter on Workers), rate limits.
Contracts
ChannelBus: stateless, ownerless event bus for channel frames.TapeAPIEscrowandServiceDirectory(not deployed).
Tooling
- Examples: minimal reader, Web2 adapter, DeFi reads, attested cross-chain reads, relay service, Cloudflare Worker, WebMCP demo.
- A black-box conformance suite for providers and relays.
- The holder console at tapeapi.fun/console: go live, publish the manifest and deploy ChannelBus from a phone wallet.
- Test vectors with an independent Python verifier; about 630 JavaScript tests and 169 Foundry tests.