Docs

Upgrading from 0.x to 1.0

1.0 is a promise: code written against the 1.0 documentation keeps working in every 1.x release. To make that promise, the interfaces were reviewed once more, and the names, shapes and error codes that would have been hard to live with were changed before the freeze. This page lists every change that can break 0.x code, what to write instead, and what 1.0 does and does not promise.

What 1.0 promises

Every export of @tapeapi/sdk and @tapeapi/server, the command-line tools tapeapi-mcp and tapeapi-verify, and the methods and result shapes of the public services (api.tapeapi.fun, relay.tapeapi.fun) are in one of three levels:

LevelMeaningHow to tell
StableNo breaking change within 1.x. Additions (a new optional option, a new field, a new error code) may come in any minor release.everything not marked otherwise
ExperimentalMay be renamed, reshaped or removed in a 1.x minor release; each change is in the changelog.@experimental in the type declarations
InternalNot part of the API; may change in any release.@internal, or not exported at all

Experimental in 1.0: everything that pays. TAP-22 payment channels and the escrow contract are not deployed or audited, and neither is the ServiceDirectory. That covers api.payer(), api.acceptPrice() / api.acceptedPrice(), the payer and maxPrice call options, the channel builders api.tx.approve / fund / requestWithdraw / cancelWithdraw / withdraw / authorizeSession / settle / setContribution / register, api.chain.escrow.*, api.chain.resolve() and api.chain.serviceOf() (and resolving a directory label; labelToBytes32, and LABEL_RE / bytes32ToLabel of abi), the directory and escrow options, contribution, MAX_CONTRIBUTION_BPS, RECOMMENDED_CONTRIBUTION_BPS and MAINNET.bem (the payment token), the voucher helpers of sig, WebMCP's paid option, and on the server the voucher store, the settlement helpers and the payment options of createProvider. Reading prices from a manifest (priceBEM, parseUnits, formatUnits) is Stable, and so are the codes PAYMENT_REQUIRED, BAD_VOUCHER and PRICE_CHANGED: a free service's client can meet them too.

Also Experimental: the whole @tapeapi/sdk/bus-privacy subpath (the root busPrivacy): busPrivacyReader, its modes, every tuning option and default (cover, contract, the pool and budget constants), scanCoverPool, plausibleRoom and the shape of stats().privacy. The defaults come from measurements on mainnet in September 2026 and may change.

Internal: abi.FUNCTIONS and abi.SERVICE_TUPLE (the ABI table follows the contracts, including the experimental ones). In ai, the helpers the reference sidecar and the website use (FORWARD_HEADERS, SESSION_HEADERS, MODEL_ID_MAX, forwardsHeader, isSessionHeader, isAnswerId, apiPath, completeOf, createSseScanner, encodeReceipt, receiptComment, pricingOf, modelEntryOf, formatOfMethod, envelopeProblems, priceProblems). Stable in ai: createVerifyingFetch, verifyUsageReceipt, validateAIField, decodeReceiptHeader, readSseReceipt, scanSse, usageOf, formatFor, sha256Hex, FORMATS, MANIFEST_FIELD, RECEIPT_HEADER, RECEIPT_METHOD, SIDECAR_ERROR_HEADER, VERIFY_ERROR_HEADER.

Breaking changes

Search your code for the name in the first column.

0.x1.0Why
A configuration or argument mistake reported as RPC_UNAVAILABLE, MANIFEST_INVALID, BAD_VOUCHER, ABI_INVALID, BAD_KEY, CHANNEL_INVALID, BAD_REQUEST, QUORUM_FAILED, METHOD_NOT_FOUND or GROUP_DELIVERY; on @tapeapi/server, a provider without rpcUrls (was INTERNAL); in WebMCP, refresh() with nothing exposed and a tool called after dispose (was BAD_REQUEST)INVALID_ARGUMENTOne code for "your own options or arguments are wrong", raised before anything is sent and never worth retrying. A retry loop on RPC_UNAVAILABLE no longer spins on a missing rpcUrls. See the table below.
e.tooLarge, e.rpcCode, e.rpcRevert, e.rpcData, e.agreed, e.disagreed, e.failed, e.groups, e.quorum, e.reasone.data.tooLarge, e.data.rpcCode, ...TapeAPIError keeps a fixed set of top-level fields (name, code, message, data, signed, httpStatus, cause, and ts, block, id, sig, error on a signed provider error). The old names still read, as deprecated aliases, until 2.0.
channel.toHex, channel.fromHex, channel.toBase64, channel.fromBase64abi.toHex (with 0x), abi.bytesToHex (without), abi.hexToBytes; base64 from your platformchannel.toHex returned hex without 0x while abi.toHex returns it with: the same name with two meanings.
channel._keySchedule, channel._busMerge, channel._busKindOfremovedTest hooks, not API.
channel.relayTransport({ api, svc, ... })channel.relayTransport({ api, service, ... })One name for a resolved service everywhere. svc is refused with a pointer here.
deliverGroupUpdate({ relay, bus }), checkGroupInvites({ relay })deliverGroupUpdate({ relayClients: [...], busClients: [...] }), checkGroupInvites({ relayClients: [...] })Always lists. These are clients to send and read through: relayClients holds { api, service, payer? } (a TapeAPI client and the resolved relay service), busClients holds { address, sendTx }. relays is something else: the list of relay references { url, container } that createGroup, resumeGroup and channel.createInvite put into a roster or an invite, a protocol field (TAP-26, TAP-27) that keeps its name. relay, bus, relays and buses are refused here with a pointer to this page.
a relay client (relayClients) { api, svc, payer }{ api, service, payer }As above.
G.createGroup({ now }), G.joinGroup({ now }) (a function returning milliseconds)clock, a function returning Unix seconds (fractional allowed); resumeGroup takes it tooEvery now in the SDK is a Unix-seconds number (channel handshakes, validateManifest, verifyUsageReceipt); a group's long-lived clock is clock, in the same unit. now on a group is refused, and so is a clock that returns milliseconds (above 1e11, e.g. Date.now).
WebMCP handle.svchandle.serviceAs above.
sig.keccak256, sig.toHex, sig.bytesToHex, sig.hexToBytesabi.keccak256, abi.toHex, ...One home for byte helpers.
readJsonBounded, describeUrl, isNodeLimit from @tapeapi/sdk/rpcremovedInternal helpers. @tapeapi/sdk/rpc exports createRpc and RPC_BODY_LIMIT.
ai.amountOf, ai.pricesOf, ai.sseDigestOfPayloads, ai.sentinelOf, ai.rootOf, ai.saltRequestBody, ai.SALT_LENGTH, ai.CURRENCIES, ai.PRICE_UNIT, ai.MODELS_MAX, ai.ENDPOINTS_MAX, ai.ALIASES_MAX, ai.PRICES_MAX, ai.AMOUNT_DECIMALS, ai.EVENT_PARSE_LIMIT, ai.FORWARD_PREFIXES, ai.SSE_RECEIPT_PREFIXremovedUsed only inside the SDK and its tests. verifyUsageReceipt does the price arithmetic and the hashing; the limits are in TAP-20 §3.9.
group.senderKey, group.buildEpochremovedThe key derivation and epoch builder behind the group handles; the TAP-27 vectors document them.
the openai-proxy subpath of @tapeapi/server, createOpenAIProxy@tapeapi/server/ai-proxy, createAIProxyAn alias whose name was wrong: the sidecar speaks Anthropic too.
createMcpEndpoint(...).handle(request).handleRequest(request)Like createProvider, createAIProxy and createMcpProxy.
createMcpEndpoint({ identity: { name } }), createMcpProxy({ identity: { name } }){ name }identity means a key pair elsewhere in the SDK. identity is refused.
UPSTREAM_TIMEOUT_MS from ai-proxy / mcp-proxyAI_UPSTREAM_TIMEOUT_MS (600 s) / MCP_UPSTREAM_TIMEOUT_MS (20 s)One name had two values.
ai.createVerifyingFetch passing a metered request to another host (localhost for 127.0.0.1)strict: INVALID_ARGUMENT before sending, naming the expected endpoint; not strict: onReport with mismatch: trueIt used to go through unverified and unreported.
ai.createVerifyingFetch with an official SDK's streamverified; strict makes the iterator throw RECEIPT_INVALIDThe official SDKs stop reading at the final event, so the old end-of-stream check never ran. A stream now ends at its final event, at [DONE] or when the connection closes, whichever comes first, and in strict mode the end is released only once a receipt that came before it verifies.
ai.createVerifyingFetch, strict, a whole answer whose receipt fails: thrown from fetch (RECEIPT_INVALID)an HTTP 502 in the API's error shape, code RECEIPT_INVALID, headers x-should-retry: false and x-tapeapi-verify-error: RECEIPT_INVALID; onReport as beforeThe official SDKs wrapped the thrown error and retried it twice by default: one bad receipt meant three requests, each possibly paid. They obey x-should-retry and throw an APIError after one. Code that calls the fetch itself checks res.ok. Retrying a paid call after other 5xx errors is your choice.
the channelRecordFloor store keyed by containerkeyed <chainId>:<container, lowercase>Other chains' clients now share the store you pass. A 0.x entry is read once and moved; nothing to do.
Group = Record<string, any> (TypeScript)GroupHandle, OwnerGroup, GroupSnapshot, RosterTyped handles.
createProvider relaxed its payment checks for allowHttp: true or a manifest with dev: trueonly createProvider({ dev: true }) relaxes them; allowHttp only allows http endpoints; the manifest's own dev field switches nothing (also in createMcpProxy)A published manifest is data, not configuration, and allowing http is not permission to skip the escrow. A dev setup passes dev: true.
createTapeAPI({ timeoutMs }), chains: { [id]: { timeoutMs } }, createProvider({ timeoutMs })rpcTimeoutMsIt is the timeout of one RPC request; api.call(..., { timeoutMs }) is the whole call and keeps its name. The old name is refused.
api.chain.tokenOf() returning tokenId as a biginta decimal stringEvery tokenId and processor the SDK returns is a decimal string; inputs still take any number, bigint or string.
an option object with a misspelt or unknown key (TypeScript)a compile errorOption interfaces (CreateProviderOptions, ChannelSelf, ChannelPeer, WebMCP's paid) no longer carry an index signature. Data shapes (Manifest, Invite, channel records) still accept extra fields.

Which mistakes are now INVALID_ARGUMENT

Where0.x code
createRpc: no URLs, a bad quorum, too few nodes or operators, no fetch; rpc.single(url) with a foreign URLRPC_UNAVAILABLE
a chain read on a client without rpcUrlsRPC_UNAVAILABLE
resolve() of an unsupported target, a bad chainId, a directory label without directory; { dev } without dev: true; forChain() of an unknown chain; chainOfContainer('nope'); refresh() of a service not from resolve(); registryKey(42)MANIFEST_INVALID
chain.channelKeys / chain.tapeSendKey given something that is not a containerCHANNEL_INVALID
payer() options, a price that is not positiveBAD_VOUCHER
tx.* arguments (an address, an amount, bps); a free service passed to a payment builder; no escrow / directory configured; publishManifest / publishChannelKeys with a bad containerABI_INVALID, MANIFEST_INVALID, CHANNEL_INVALID
callQuorum with no services, a bad quorum or onDissent, a service not from resolve(), a malformed compareQUORUM_FAILED, BAD_REQUEST
deliverGroupUpdate / checkGroupInvites carriers, invite, self, cursors, a foreign updateGROUP_DELIVERY
createProvider: no signerKey, methods not an object or missing a handler, a bad escrow, rateLimit, minVoucherLifeSBAD_KEY, METHOD_NOT_FOUND, MANIFEST_INVALID
createAIProxy / createMcpProxy optionsBAD_REQUEST, BAD_KEY, MANIFEST_INVALID
createVerifyingFetch without a service; exposeTapeAPI / manifestToTools paid options; createMcpServer without infoMANIFEST_INVALID, BAD_REQUEST

The public service keeps its shapes: tapeName on api.tapeapi.fun still answers processor as a number (changing its declared returns would change the on-chain manifest and make every tapeapi-mcp pin of it refuse the service).

The codec layers are unchanged: abi reports ABI_INVALID and canon reports CANON_INVALID whether the bytes came from you or from the network. Protocol refusals of callQuorum (fewer than two providers, two services that share a holder or an origin, an attested read without a block) stay QUORUM_FAILED.

Three modules keep their own code for a caller's mistake throughout 1.x, so match these as well: the channel module (channel.*, TAP-26) reports CHANNEL_INVALID, the group module (group.*, TAP-27) reports GROUP_INVALID, and the checks api.call() makes on params and id before sending report BAD_REQUEST, the code a provider would answer for the same request. None of them is worth retrying.

Error codes

The full list. Provider codes travel in signed envelopes and never change meaning (TAP-21 §3.2); client codes are raised by the SDK (§3.4).

CodeKindMeaningRetry?
PAYMENT_REQUIREDproviderThe method is priced and no voucher came, or payments do not run on that chainno
BAD_VOUCHERproviderThe voucher was refused (data.lastCumulative, data.voucher)the SDK resyncs once
METHOD_NOT_FOUNDproviderNo such methodno
BAD_REQUESTproviderThe request itself (id, params) is malformed; signed when the provider says sono
INTERNALproviderThe provider failed; may carry data.revertyes
TOOLS_CHANGEDproviderAn MCP-bound service's upstream tools no longer match toolsSha256no
RPC_DISAGREEclientNodes gave different answersyes
BAD_SIGNATUREclientAn envelope-binding check failedthe SDK re-reads the manifest
MANIFEST_INVALIDclientThe manifest read from the chain or the endpoint is not validno
DELEGATION_INVALIDclientThe delegation is missing, expired or not the holder'sno
PROVIDER_UNAVAILABLEclientTransport failure; data.timedOut / data.aborted for your own timeout or abortyes
RATE_LIMITEDclientHTTP 429 (data.retryAfterS)after the wait
PRICE_CHANGEDclientThe price rose above what you acceptedafter consent
QUORUM_FAILEDclientProviders disagree, or too few answered (data.agreed, data.failed, ...)depends
ATTEST_DISAGREEclientAn attested read disagreesno
NOT_FOUNDclientNo service there: an unregistered label, a processor number past the last, or no channel record (a missing manifest file is MANIFEST_INVALID)no
RPC_UNAVAILABLEclientToo few nodes answeredyes
RPC_STALEclientSince 1.2, with the experimental pin option: the block the nodes confirm is older than maxPinAgeS, or ahead of this client's clock (data.ageS)yes
CONTRACT_UNKNOWNclientSince 1.2, with the experimental sentinel: 'strict': a TapeOut identity contract runs an implementation this SDK does not know, which means it was upgraded (data.role, data.implementation)no: update the SDK or check the upgrade
PROOF_INVALIDclientSince 1.3, with the experimental proofs (needs pin): a verified Merkle proof of fileInfo, cpuAt, isCPU or ownerOf proves a value other than the one the nodes answered (with proofs: true as well as 'strict'); or, with 'strict' only, every proof the nodes served failed to verify against the stateRoot of the block the nodes confirmed (data.read, data.node, data.block, data.stateRoot)no
PROOF_UNAVAILABLEclientSince 1.3, with the experimental proofs: 'strict': no node served eth_getProof for the pinned block, nodes of quorum operators did not agree on a stateRoot, or the contract runs an implementation whose storage layout the SDK does not know (data.read, data.reason). With proofs: true the same is only a warning and the quorum's answer is kept (detection only)yes, or add a node that serves proofs
RPC_ERRORclientEvery node returned the same JSON-RPC error (data.rpcCode, data.rpcRevert)a revert: no
CANON_INVALIDclientJSON with no canonical form, duplicate or forbidden keysno
ABI_INVALIDclientABI data that does not decodeno
BAD_KEYclientA key or key address that cannot be usedno
CHANNEL_INVALIDclientTAP-26 data invalid or not authorised by the current holderno
GROUP_INVALID, GROUP_EQUIVOCATIONclientTAP-27 data invalid; the owner signed two epochs with one numberno
GROUP_DELIVERYclientSome group post failed after every post was tried (data is the delivery result)depends
BAD_RESPONSEclientA relay or your wallet's sendTx answered something unusableno
BUS_PRIVACY, BUS_BUDGETclientToo few cover rooms; a contract-wide read over its budgetno
TAPESEND_INVALIDclientA TAP-10 payload is invalidno
COMPARE_PATH_INVALIDclientOne provider's result has no number at a compare pathno
RECEIPT_INVALIDclientAn AI usage receipt is missing or fails a checkno
BUDGET_EXCEEDED, USER_DECLINEDclientWebMCP spending budget; the user said nono
INVALID_ARGUMENTclientYour own options or arguments are wrongnever
METHOD_NOT_ALLOWEDprovider route, unsignedHTTP 405: a request other than POST to /tapeapi/v1/<method>. The SDK always POSTs; a client that meets it treats it as a transport failure (PROVIDER_UNAVAILABLE)no
NAME_TAKENWebMCPIn handle.skipped[].code: registerTool failed, usually because another script on the page already registered that tool name (reason says why). Not thrownafter the other tool is gone (refresh())

An MCP server in front of a service (createMcpProxy) reports its own refusals as JSON-RPC errors whose error.data.code is TOOLS_CHANGED, INVISIBLE_CHARACTERS or UPSTREAM_UNAVAILABLE.

Formats that are now frozen

These are written to your storage by the SDK or the tools, so 1.x reads what 1.0 wrote:

  • channelRecordFloor: key <chainId>:<container, lowercase>, value an integer (Unix seconds).
  • checkGroupInvites cursors: key relay:<relay container>:<room>, value { after, epoch }.
  • A group snapshot(): { v: 1, gid, owner, epoch, role, lastSeq?, roster? }.
  • The tapeapi-mcp pin file (~/.tapeapi/mcp-pins.json, v: 1).

Command-line tools

tapeapi-mcp and tapeapi-verify keep their options. Both exit with 0 (normal exit), 1 (a runtime failure) or 2 (a usage mistake), and read no environment variable. tapeapi-verify --strict now ends a stream in an error event when no receipt that came before its end (its final event or [DONE]) verifies, and passes on only whole events, so a stripped receipt fails the stream instead of arriving after it ended.

Edit this page on GitHub