For the complete documentation index, see llms.txt. This page is also available as Markdown.

API Changelog & Migration

This page tracks breaking changes, deprecations, and additions across the Molecule APIs. Use it when upgrading an existing integration. Each API's most recent changes are listed first.


Authentication

The service-token sign-in message is now single-use and expires

getServiceSignInMessage used to return a deterministic string β€” a pure function of (walletAddress, serviceName) β€” which meant one captured signature could mint fresh tokens indefinitely. The message now embeds a server-issued single-use nonce and its expiry, and generateServiceToken verifies and consumes that nonce:

  • The message is valid for 10 minutes from issuance. The new expiresAt field on getServiceSignInMessage reports the deadline.

  • The nonce is consumed on first successful redemption. Issuing a second token requires a fresh message and a fresh signature.

  • There is one outstanding nonce per (walletAddress, serviceName), last-write-wins β€” calling the query again invalidates an unredeemed message.

  • Signatures over the older nonce-free message format no longer verify.

Failures come back as UNAUTHENTICATED with details.reason one of NONCE_NOT_FOUND (never requested, or already consumed), NONCE_EXPIRED, or INVALID_SIGNATURE (altered text, a message superseded by a later call, or a walletAddress that is not the signer).

Migration: Fetch the message immediately before signing, and treat every one of those reasons as "request a new message and sign it again" rather than as a retryable call β€” re-submitting the same signature can never succeed. Callers that already ran getServiceSignInMessage β†’ sign β†’ generateServiceToken back to back need no change; callers that cached the message, cached a signature, or reconstructed the string client-side must stop doing so. See Obtaining a Token.

Service tokens are self-issued, and scoped to their own lifecycle

Two clarifications and one hardening, all now reflected across the API docs:

  • Nobody provisions a service token for you. generateServiceToken accepts a wallet signature, so any caller mints its own: getServiceSignInMessage β†’ sign the message verbatim (EIP-191 personal_sign) β†’ generateServiceToken. The only credential that still comes from the Molecule team is the mol_ consumer credential. Earlier pages described service tokens as team-issued; that was never the only path and is no longer the documented one.

  • A service token is bound to a wallet, not to a lab. Docs that described it as identifying "which lab you have write access to" were wrong. It carries a wallet identity; what it may do on a given lab is resolved per request from that wallet's live onchain role. One token therefore works across every lab the wallet has a role on, and a role granted after issuance takes effect without re-issuing.

  • extendServiceToken and revokeServiceToken are scoped to the caller's own tokens. The presented token must own the tokenId it names; a tokenId belonging to another wallet returns the byte-identical NOT_FOUND of one that does not exist, so token existence cannot be enumerated.

Migration: None required if you already self-issue. If you hold a team-provisioned token, it keeps working β€” but you can mint and rotate your own. If you built per-lab token issuance, you can collapse it to one token per wallet. If any code called extendServiceToken / revokeServiceToken for a tokenId issued to a different wallet, it now receives NOT_FOUND. See Authentication and Service Tokens.

Contributor role parity for service-token content writes

The six content-write mutations β€” initiateCreateOrUpdateFile, finishCreateOrUpdateFile, deleteDataRoomFile, updateFileMetadata, moveEntry and createAnnouncement (announcements have since been deprecated) β€” now gate a service token on the Contributor role, matching the Privy user path per mutation. Previously the service path required lab ownership for these, which meant a wallet granted Contributor on a lab could act through a user session but not through its own service token.

This is what unblocks the "human owns the lab, agent contributes to it" flow: the owner grants the agent's wallet Contributor, and the agent's self-issued token can write. Owner-gated surfaces are unchanged β€” createLab, updateLabNftMetadata and generateLabImageUploadUrl still require ownership.

Migration: None β€” this is a widening. Note that role state reaches the API through an event indexer, so a write can still return UNAUTHORIZED for a few seconds after a grant confirms onchain; retry with backoff rather than re-issuing the token. Walkthrough: Agent access.

Backend credential stores confined to the platform network

The data stores behind API authentication β€” the consumer credential registry, machine service tokens, and the access whitelist β€” are now network-confined to Molecule's private cloud network. They are unreachable from outside it, even with valid cloud-account credentials; only the API's own backend can read or write them. This is a hardening change with no effect on any API, header, token format, or SDK β€” consumer credentials, X-Service-Token, and Privy user tokens all work exactly as before.

Migration: None required.

x-api-key replaced by consumer credentials

All Molecule APIs (Labs, Tokenization β€” they share one GraphQL endpoint) now authenticate with a consumer credential instead of an x-api-key header. A consumer credential has the shape mol_<consumerId>_<secret> and is sent directly as the Authorization header value, with no Bearer prefix.

Migration: Request a consumer credential (see Getting Started) and send it as Authorization: mol_<consumerId>_<secret> instead of x-api-key β€” do not prefix it with Bearer, which is reserved for Privy user tokens and will fail authentication. Nothing else changes: X-Service-Token for machine-authorized mutations, and Authorization: Bearer <Privy token> + x-wallet-address for user-authorized mutations, work exactly as before. See Authentication for the full header reference.


Labs API

Lists without paging arguments now fail above a size limit

Nine list fields return every item and take no first / limit argument: Lab.members, LabRef.members, ListLabMembersResult.members, DataRoom.files, Announcement.attachments, SearchLabsAnnouncement.attachments, OnChainEvent.events, FileCategoriesAndTagsResult.data and FileCategory.tags. Each now has a limit of 1000 items, stated in its field description. At or under the limit nothing changes. Above it, a query that selects the list fails with COMPLEXITY_LIMIT_EXCEEDED, details.reason: "RESULT_CARDINALITY_LIMIT", plus details.field (for example DataRoom.files) and details.limit. A shortened list is never returned as if it were complete.

What fails depends on the list. Lab.members and LabRef.members fail on their own: that lab's members is null with the error at its path, and the rest of the response loads. Every other list fails the root field that returned it: one onChainActivity transaction over the events limit fails the page. A query that does not select the list is not affected. Limits sit well above anything production serves today. Token.relations and Token.markets have worked this way, with a limit of 50, since they were introduced.

Migration: No action required for current data. Treat the error as non-retryable: drop the list from the selection (an onChainActivity page without events always loads, and its last id is the cursor past the transaction), or narrow the query (a smaller page, a single lab). If you expect to exceed a limit, contact the Molecule team to raise it.

Announcements are deprecated

Announcements are no longer surfaced in the Molecule app, and they are out of every tutorial, how-to and feature-description page. The API surface is unchanged and still works β€” nothing has been removed from the schema and no call has started failing. This is a "stop building on it" notice, not a breaking change.

Still live, and still returned to callers who ask for it:

Surface
Status

createAnnouncement mutation

Live. Gated on Contributor, like the file writes

/x402/labs/createAnnouncement gateway endpoint

Live, still in X402_WRITE_MUTATIONS, still priced

LabEventAnnouncement in the LabActivityNode union

Live β€” returned by unfiltered labActivity / activities

SearchLabsAnnouncementHit in the SearchLabsHit union

Live β€” returned by searchLabs

LabActivityFilter.ANNOUNCEMENT

Live

Migration: None required; existing integrations keep working. Do not add new dependencies on announcements. If you consume labActivity or activities and want a file-only feed, pass filter: FILE rather than assuming one β€” the unfiltered feed still contains announcement nodes for labs that have them. If you switch on __typename across LabActivityNode or SearchLabsHit, keep the announcement arms handled. See Browse & Search.

Assignment Agreement is no longer a gate, and is out of the API docs

Signing the assignment agreement is not a precondition for createLab, for uploading files, or for any other Labs API operation. It was previously presented as a required onboarding step, and the <AGREEMENT_TYPE>_NOT_SIGNED / AGREEMENT_CHECK_UNAVAILABLE failure causes are now dormant β€” reserved in the catalogue, but not emitted.

The legalAgreement* operations remain in the schema and are unchanged, but they have been removed from every onboarding and reference flow, and Legal Agreements is out of the site navigation. Do not build a new integration around them.

Migration: Delete any agreement-signing step from your workflow β€” it does nothing. If you branch on an agreement status before writing, remove the branch. Nothing in the API changed; only what is required of you did. The onboarding tutorials no longer include the step: Tutorials.

Staging introspection is the supported way to get the schema

Production has introspection disabled (below), but staging has it enabled β€” point codegen, a playground or an SDK generator at https://staging.graphql.api.molecule.xyz/graphql with your consumer credential and generate normally. Both environments serve the same schema, so generate against staging and point the generated client at production.

This supersedes the earlier guidance to request a copy of the schema from the Molecule team.

Migration: If your codegen currently fails against production, repoint it at staging. See Getting the schema.

GraphQL introspection disabled and query depth capped in production

The production endpoint (shared by all Molecule APIs β€” see API Overview) no longer serves __schema / __type introspection queries: they now return a validation error. __typename still resolves. Selection-set depth is also capped at 10 in production, with scalar leaves counted as a level ({ root { child { name } } } is depth 3). A query beyond that limit fails at execution time with errorType: "QueryDepthLimitReached" and partial data β€” a plain GraphQL error, not the catalogued error shape used elsewhere, so handle both.

Migration: If your codegen or tooling discovers the schema by introspecting the production endpoint, that now fails β€” introspect staging instead, where it is enabled, and point the generated client at production (see Getting the schema). If you see QueryDepthLimitReached, flatten the query to 10 levels of nesting or fewer; this limit was not previously enforced.

isSuccess removed (unified error contract)

The Labs API now reports failure one way per operation class, and the isSuccess envelope is gone:

  • Queries throw. A failed query surfaces as an entry in the top-level GraphQL errors[] array; the failed field is null, and because most Labs query fields are non-null the null propagates and data itself comes back null (only labWithDataRoomAndFiles and dataRoomFile null just their own field). errorType carries the error code (table below) β€” the only value to branch on β€” and errorInfo { requestId, retryable, details } carries the correlation id and structured context. Query result types (ActivitiesResult, FileCategoriesAndTagsResult, ListLabMembersResult, DidLinkStatusResult, LegalAgreementTemplateResult, ServiceSignInMessageResult) no longer have an isSuccess or an error field.

  • Mutations return errors in-band. Every mutation *Result carries a nullable error: ApiError. Success ⇔ error == null. A top-level errors[] entry on a mutation means a transport or infrastructure failure (or an invalid request document), not a business outcome. Where a result keeps a top-level message, it mirrors error.message on failure and is never empty.

isSuccess has been removed from every Labs result type, and the error field on mutation results is now typed ApiError (the previous error type no longer exists). A document that still selects isSuccess is rejected at validation (Field 'isSuccess' in type '…Result' is undefined) and never executes.

Behaviour that changed alongside the envelope:

  • Auth denials are code-accurate. The former AUTH_FAILED catch-all is split into UNAUTHENTICATED (missing, invalid or expired credentials), UNAUTHORIZED (authenticated but lacking the role or membership), NOT_FOUND (the lab does not exist) and VALIDATION_FAILED (malformed input).

  • Legacy codes live on as details.reason. Codes such as PROJECT_NOT_FOUND, LAB_NOT_FOUND, INVALID_OCL_ID or SHORTNAME_TAKEN are no longer top-level codes; they survive as the reason key inside details, beneath the catalogue code (for example CONFLICT with reason: "SHORTNAME_TAKEN"). Branch on code first and on reason only where a page documents it.

  • Service tokens. ServiceTokenResult and ServiceTokenRevocationResult gained error: ApiError; token, tokenId, serviceName, expiresAt, createdAt and revokedAt are null on failure instead of "".

  • InitiateFileUploadResult.method is nullable and null on failure.

  • LegalAgreementStatusResult is dual-surface. As the legalAgreementStatus(oclId, type) query, failures throw and error is always null. As the legalAgreementStatus field on Lab / LabRef, an upstream failure returns the payload with error set (typically UPSTREAM_UNAVAILABLE) instead of nulling the lab: error != null means the status could not be determined, and only when error == null does signed: false mean "not signed".

  • listLabMembers on an unknown lab throws NOT_FOUND (previously a PROJECT_NOT_FOUND envelope). createLab conflicts return CONFLICT with details.reason PROJECT_CONFLICT (lab already registered) or ACCOUNT_NAME_CONFLICT.

The Tokenization API is unaffected. Its operations keep their previous result envelope (a success flag alongside an error object), exactly as documented in the Tokenization API reference. This section applies to the Labs API only.

ApiError

On an in-band mutation error, details arrives as a JSON-encoded string (AppSync AWSJSON), e.g. "details": "{\"reason\":\"NOT_LAB_OWNER\"}"; on a thrown query error, errorInfo.details is a plain object. The in-band string is currently encoded twice, so read it with the tolerant parseDetails rather than a single JSON.parse, which returns another string and makes .reason silently undefined. Ignore keys you do not recognise, and never match on message β€” its wording may change without notice.

Error codes

Code

retryable

Meaning

Typical details.reason

UNAUTHENTICATED

false

Missing, invalid or expired credentials.

AUTH_FAILED, SERVICE_AUTH_FAILED, INVALID_SIGNATURE, NONCE_NOT_FOUND, NONCE_EXPIRED, WALLET_MISMATCH, CONSUMER_CREDENTIAL_REQUIRED

UNAUTHORIZED

false

Authenticated but not allowed (role or membership).

UNAUTHORIZED (role/membership denial), NOT_LAB_OWNER, MUTATION_NOT_ALLOWED

NOT_FOUND

false

The referenced resource does not exist.

LAB_NOT_FOUND, OCL_NOT_FOUND, TOKEN_NOT_FOUND, PROJECT_NOT_FOUND, …

VALIDATION_FAILED

false

Input failed validation; details.field names the offending field.

INVALID_OCL_ID, INVALID_INPUT, MISSING_INPUT, …

CONFLICT

false

A valid request conflicts with current state.

SHORTNAME_TAKEN, PROJECT_CONFLICT, ACCOUNT_NAME_CONFLICT, ALREADY_REVOKED

FAILED_PRECONDITION

false

Resource state makes the operation impossible until that state changes.

TOKEN_REVOKED, LEGACY_ENCRYPTION, NOT_ENCRYPTED, MISSING_DEK

COMPLEXITY_LIMIT_EXCEEDED

false

Query shape or result size is over the limit.

COMPLEXITY_LIMIT_EXCEEDED, RESULT_CARDINALITY_LIMIT (list limits)

RATE_LIMITED

true

Throttled β€” retry with backoff.

β€”

TIMEOUT

true

Execution exceeded the request budget.

β€”

UPSTREAM_UNAVAILABLE

true

A dependency failed β€” retry with backoff.

KAMU, CMS, IPFS

INTERNAL_ERROR

true

Unexpected failure; quote requestId when reporting it.

TOKEN_GENERATION_FAILED, CREATE_LAB_FAILED, UPLOAD_INIT_ERROR, KMS_ERROR, …

Codes may be added over time, and each addition is published on this page. Treat a code you do not recognise as non-retryable, keep the raw value for diagnostics and surface it to a human. details.reason values are diagnostic refinement, not a contract surface β€” they may be extended without notice. For errors outside the GraphQL API, such as x402 Gateway responses, see Errors.

Before / after

Migration: Remove isSuccess (and, except on legalAgreementStatus, any error { … } selection) from every Labs query document β€” a document that still selects it fails validation and the query never runs β€” and handle failures from the top-level errors[] array, keyed on errorType. On mutations, replace isSuccess with error { code message requestId retryable details }, treat error == null as success, and branch on error.code (plus details.reason where documented), never on message text. Replace any check on the former AUTH_FAILED catch-all with the split codes listed above, and any token === "" / tokenId === "" checks on service-token results with null checks. If you read legalAgreementStatus off a lab object, select error { code message } and check it before trusting signed. Leave your Tokenization API handling as it is. See Errors for the full reference.

*V2 operations and pre-OCL naming removed

The legacy *V2 operations and the pre-OCL naming have been removed. The current API is oclId-based. If you are migrating from an older integration, use the current names below.

Renamed queries

Legacy (removed)
Current
Notes

projects / projectsV2

labs

Same paginated shape; adds an optional role filter

projectWithDataRoomAndFiles / …V2

labWithDataRoomAndFiles

Look up by oclId (or shortname) instead of ipnftUid

dataRoomFileV2

dataRoomFile

Identified by oclId + path

projectActivity / projectActivityV2

labActivity

β€”

projectAnnouncementsV2 / projectAnnouncementV2

labActivity / activities

Use the filter: ANNOUNCEMENT argument (announcements since deprecated)

activitiesV2

activities

β€”

Renamed mutations

Legacy (removed)
Current
Notes

createProject

createLab

Now takes input: { oclId } instead of ipnftSymbol / ipnftTokenId

createAnnouncementV2

createAnnouncement

Takes oclId; the legacy moleculeAccessLevel param was removed. Announcements since deprecated

initiateCreateOrUpdateFileV2

initiateCreateOrUpdateFile

β€”

finishCreateOrUpdateFileV2

finishCreateOrUpdateFile

β€”

updateFileMetadataV2

updateFileMetadata

β€”

deleteDataRoomFileV2

deleteDataRoomFile

β€”

Renamed fields

Top-level identifiers on Lab / LabRef were renamed away from the legacy IP-NFT naming:

Legacy field
Current field
Notes

ipnftUid

oclId

Now a 32-byte hex string (0x…), not <address>_<tokenId>

ipnftSymbol

shortname

Human-readable slug derived from the lab name

ipnftAddress

labAccountAddress

ERC-6551 token-bound account address

ipnftTokenId

labNftTokenId

LabNFT tokenId (decimal string)

The linked legacy IP-NFT (for labs migrated from one) is still available as the nested ipnft object on Lab / LabRef; LabRef additionally exposes a scalar ipnftId field.


x402 Gateway

Gateway base URLs published, and the 402 challenge is a header

The staging and production gateway base URLs are now published on the x402 Gateway page β€” they no longer have to be requested.

Along with them, one correction that matters for anyone implementing the handshake: the payment requirements arrive as base64-encoded JSON in the payment-required response header, not in the 402 response body. The body is only {"isSuccess":false,"message":"Payment required"}. Client code that parsed the body for accepts never saw a price.

Migration: Read the challenge from the payment-required header and base64-decode it; take amount, asset, network and payTo from accepts[0]. amount is in the asset's smallest unit (USDC has 6 decimals, so "10000" is $0.01). Worked example: Reading the 402 challenge.

Also worth knowing: payment buys a short-lived service token for the payer wallet, not a role. A mutation the payer is not authorized for returns 200 with error.code: "UNAUTHORIZED" and is still settled β€” check the target lab and your role on it (both free, public queries) before signing.


February 2026

Breaking Changes

Schema Flattening β€” metadata wrapper removed from IPNFT and IPT

The intermediate metadata wrapper objects (IPNFTMetadata, IPTMetadata) have been removed. All metadata fields now live directly on the IPNFT and IPT types.

Migration: Remove all metadata { ... } wrappers and access fields directly on the parent type.

fundingAmount JSON field decomposed into 4 typed fields

The fundingAmount JSON field on IPNFT has been replaced with 4 strongly-typed fields:

Migration example:

agreements changed from JSON array to typed relation

The agreements field on IPNFT has changed from a JSON array to a queryable typed relation with sub-field selection:

Migration: Update your queries to select specific agreement fields instead of receiving a raw JSON array.

Filter changes β€” nested metadata filters removed

All filter paths that previously went through metadata are now direct fields:

researchLead and originalOwner filter paths changed

These relation filters are now direct on the parent type instead of nested inside metadata context:

New Features

New query types

The following new top-level queries are now available:

Query
Description

user(id) / users(...)

Query users by address, list associated IP-NFTs and IPTs

researchLead(id) / researchLeads(...)

Query research leads and their associated IP-NFTs

chain(id) / chains(...)

Query blockchain networks and their markets

agreement(id) / agreements(...)

Query legal agreements associated with IP-NFTs

All new queries support limit, skip, sortBy, sortOrder, and filterBy parameters.

New fields on IPNFT

  • updatedAt β€” Last update timestamp

  • tokenUri β€” Token metadata URI

  • symbol β€” Token symbol

  • schemaVersion β€” Metadata schema version

  • userId β€” Owner user ID (for direct filtering)

  • researchLeadId β€” Research lead ID (for direct filtering)

  • fundingAmountCurrency, fundingAmountValue, fundingAmountDecimals, fundingAmountCurrencyType β€” Decomposed funding amount fields

New fields on IPT

  • updatedAt β€” Last update timestamp

  • mintedAt β€” Minting timestamp

  • agreementMimeType β€” Agreement file MIME type

  • originalOwner β€” Full User type (with id, address)

  • originalOwnerId β€” Original owner user ID (for direct filtering)

  • ipnftId β€” Parent IP-NFT ID (for direct filtering)

  • links β€” Related links

  • capped β€” Whether token issuance is capped

New fields on Market

  • createdAt, updatedAt β€” Timestamps

  • inverted β€” Whether the pair is inverted

  • iptId β€” Associated IPT ID (for direct filtering)

  • chain.logoUrl β€” Chain logo URL now available

agreements queryable as typed relation

Agreements on IP-NFTs are now a fully queryable relation with sub-field selection, filtering, sorting, and pagination:

Need help?

Ask the team on Discord. Reporting an API error? Include the requestId from the failing response so we can trace it.

Ask on Discord

Page wrong, unclear or missing something? Let us know with Was this helpful? on this page.

Last updated

Was this helpful?