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

πŸ€–For Agents: One-Pager

The whole default Labs flow on one page, no prose β€” the page to paste into a system prompt.

The complete default flow β€” self-issue a token, mint a lab, upload a public file, verify β€” with nothing else on the page. Copy it into a system prompt or a context file. For the same flow with expected responses, failure modes and the encrypted variant, use the tutorials.

Constants

Staging (Base Sepolia) β€” everything on this page runs against these:

GRAPHQL_URL              https://staging.graphql.api.molecule.xyz/graphql
CHAIN                    baseSepolia (84532)
ONCHAIN_LAB_FACTORY      0xd629FE2310b4309a212495F10A47f8436dcEfD90
LABNFT                   0x13Ff210695fdb54A7F928ECcc28BC3486c05BB28
ACCESS_RESOLVER          0x5493F472602C87318EA5Eff753cDD593bf9bF559
ACCESS_CONDITION_CHAIN   "baseSepolia"
LAB_PAGE                 https://testnet.labs.molecule.xyz/projects/<slug>

Production (Base) β€” swap these in, nothing else changes:

GRAPHQL_URL              https://production.graphql.api.molecule.xyz/graphql
CHAIN                    base (8453)
ONCHAIN_LAB_FACTORY      0xECdF4f05384056507485C90aeAb0a83268760D6E
LABNFT                   0x9F96027eeAFb9ad5F2b5d7043B36Ee96B2EeBE92
ACCESS_RESOLVER          0x89a14Be8f7824d4775053Edad0f2fA2d6767b72B
ACCESS_CONDITION_CHAIN   "base"
LAB_PAGE                 https://labs.molecule.xyz/projects/<slug>

Headers

Public queries take Authorization alone. Sending X-Service-Token on a public query is unnecessary; sending an empty one is worse than omitting it.

Error contract

This is the Labs API contract. The Tokenization API and the x402 Gateway report errors differently; all three are on Errors.

  • Queries throw. Failure lands in top-level errors[]. Branch on errors[i].errorType. errors[i].errorInfo is { requestId, retryable, details } and details is already an object.

  • Mutations return errors in-band. Every *Result has error: ApiError. Success ⇔ error == null. Select error { code message requestId retryable details } on every mutation. A top-level errors[] entry on a mutation means it didn't run: an invalid document, an infrastructure failure, or RATE_LIMITED.

  • RATE_LIMITED is thrown on every operation, mutations included. Wait errorInfo.details.retryAfterSeconds, retry once, then back off up to budgetWindowSeconds. Never tight-loop: retries are charged too. Not enforced yet.

  • Parse details tolerantly. It is an object on thrown query errors, a JSON string in-band, and currently a doubly-encoded JSON string in-band β€” one JSON.parse there returns a string, and .reason on it is silently undefined. Loop until it is not a string:

  • Branch on code, never on message. Retry only when retryable is true, with exponential backoff. Quote requestId in any bug report.

  • One exception to "retry when retryable": a malformed or out-of-bounds expiresIn on generateServiceToken is not pre-validated and comes back as INTERNAL_ERROR / details.reason: TOKEN_GENERATION_FAILED, which is flagged retryable but never will be. Validate expiresIn client-side (<int><unit>, unit in s m h d w M y, 1 hour to 2 years) and cap retries on that reason.

  • Codes: UNAUTHENTICATED, UNAUTHORIZED, NOT_FOUND, VALIDATION_FAILED, CONFLICT, FAILED_PRECONDITION, COMPLEXITY_LIMIT_EXCEEDED, RATE_LIMITED, TIMEOUT, UPSTREAM_UNAVAILABLE, INTERNAL_ERROR (* = retryable). An unrecognised code: treat as non-retryable, preserve it, surface it.

Step 1 β€” Self-issue a service token

Sign message verbatim with the wallet as a plain personal message (EIP-191 personal_sign, not typed data). message embeds a single-use nonce valid for 10 minutes β€” fetch it immediately before signing, never cache it or the signature, never rebuild the string yourself, and fetch a fresh one for every token. Then:

token β†’ X-Service-Token on everything after this. expiresIn defaults to 180d; format <int><unit> with unit one of s m h d w M y; bounds 1 hour to 2 years. The token is wallet-bound, not lab-bound: authorisation is resolved per request from that wallet's onchain role on the lab you name.

Step 2 β€” Mint the LabNFT (onchain)

Read oclId off OclIdentityCreated. The event fires on LabNFT, not the factory β€” filter receipt logs to the LabNFT address before decoding.

Step 3 β€” Register the lab

CONFLICT / reason: PROJECT_CONFLICT means the lab is already registered β€” treat as success and continue.

Step 4 β€” Upload a public file (3 calls)

PUT the raw bytes to uploadUrl with exactly the returned headers (key/value pairs) and the returned method. Presigned URLs expire in ~15 minutes.

  • accessLevel: "PUBLIC" | "HOLDERS" | "ADMIN".

  • changeBy: the caller's wallet address.

  • path for a new file (no underscores), ref for a new version of an existing one. Use one or the other, never both.

  • Valid tags / categories come from the public fileCategoriesAndTags query.

Step 5 β€” Verify

Public query, Authorization only. Your path is in dataRoom.files. A null result means the lab is not registered β€” this query is nullable and does not throw for a missing lab.

The lab page slug is lab-<labNftTokenId> until the lab is renamed, then the shortname derived from its new name; the lab-<tokenId> form stops resolving at that point. Never use oclId as a slug β€” it does not resolve.

Encrypted files, in four lines

  1. generateDataEncryptionKey β†’ { plaintextDEK, encryptedDek, encryptionSystem }.

  2. AES-256-GCM the bytes locally with plaintextDEK and a fresh 12-byte IV; discard the plaintext key.

  3. finishCreateOrUpdateFile with accessLevel: "HOLDERS" (or "ADMIN") and encryptionMetadata: { encryptionSystem, encryptedDek, iv, contentHash, accessControlConditions, encryptedBy, encryptedAt } β€” echo encryptionSystem verbatim, never hardcode it.

  4. To read it back: decryptDataKey(oclId:, filePath:) β†’ { plaintextDEK, iv } after the backend re-evaluates the file's onchain access conditions against your wallet.

Full recipe including accessControlConditions: Upload an encrypted file.

Rules that break runs when ignored

  1. No Bearer in front of a mol_ credential.

  2. Sign the sign-in message verbatim; any reformatting fails verification. It is single-use and expires after 10 minutes, so a cached message or signature returns UNAUTHENTICATED β€” NONCE_NOT_FOUND once redeemed, NONCE_EXPIRED past the window, INVALID_SIGNATURE if a later fetch superseded it. The fix is always a fresh getServiceSignInMessage, never a retry of the same signature.

  3. Success on a mutation is error == null β€” never a truthy payload field, and never a message string.

  4. Read error.details with the tolerant parseDetails above β€” never a bare JSON.parse. In-band it is a JSON string (currently doubly encoded); on thrown query errors it is already an object.

  5. Filter mint receipt logs to the LabNFT address before decoding OclIdentityCreated.

  6. Send the presigned PUT with the returned headers unchanged, and the raw bytes as the body.

  7. Writing into a lab you do not own needs a Contributor role on it β€” see Agent access. After a role grant, an indexer lag of a few seconds can still return UNAUTHORIZED; retry with backoff.

  8. A successful createLab does not mean the lab is writable yet. Step 4's first call can return NOT_FOUND ("Project 0x… does not exist") for a few seconds, because createLab falls back to an onchain ownership check while the file mutations read the indexed record. Retry NOT_FOUND with backoff on the first write after a mint; do not re-run createLab, which then returns CONFLICT.

  9. Three addresses, not interchangeable: your own wallet (walletAddress, changeBy), the human owner's (x-wallet-address, their path only), and the Lab's OCL account (labAccountAddress, and the account argument in access conditions). Putting an owner's address where labAccountAddress belongs uploads fine and then locks everyone out of the file, with no error saying why. oclId is none of them β€” it is a lab id whose trailing 40 hex chars happen to be the OCL account address. See the three wallets.

  10. Production has introspection off and a depth limit of 10. Generate types against staging.

  • The three wallets β€” owner vs agent vs OCL account, and which field each address goes in

  • Getting Started β€” how to interact with our products, prerequisites, costs

  • Glossary β€” every term used here, defined in a sentence

  • Tutorials β€” the same flow with responses and failure handling

  • Labs API β€” full operation reference

  • Molecule Skill β€” the same workflow as MCP tool calls

  • x402 Gateway β€” pay per call, no service token

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?