π€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 onerrors[i].errorType.errors[i].errorInfois{ requestId, retryable, details }anddetailsis already an object.Mutations return errors in-band. Every
*Resulthaserror: ApiError. Success βerror == null. Selecterror { code message requestId retryable details }on every mutation. A top-levelerrors[]entry on a mutation means it didn't run: an invalid document, an infrastructure failure, orRATE_LIMITED.RATE_LIMITEDis thrown on every operation, mutations included. WaiterrorInfo.details.retryAfterSeconds, retry once, then back off up tobudgetWindowSeconds. Never tight-loop: retries are charged too. Not enforced yet.Parse
detailstolerantly. It is an object on thrown query errors, a JSON string in-band, and currently a doubly-encoded JSON string in-band β oneJSON.parsethere returns a string, and.reasonon it is silentlyundefined. Loop until it is not a string:
Branch on
code, never onmessage. Retry only whenretryableistrue, with exponential backoff. QuoterequestIdin any bug report.One exception to "retry when retryable": a malformed or out-of-bounds
expiresInongenerateServiceTokenis not pre-validated and comes back asINTERNAL_ERROR/details.reason: TOKEN_GENERATION_FAILED, which is flagged retryable but never will be. ValidateexpiresInclient-side (<int><unit>, unit ins 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.pathfor a new file (no underscores),reffor a new version of an existing one. Use one or the other, never both.Valid
tags/categoriescome from the publicfileCategoriesAndTagsquery.
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
generateDataEncryptionKeyβ{ plaintextDEK, encryptedDek, encryptionSystem }.AES-256-GCM the bytes locally with
plaintextDEKand a fresh 12-byte IV; discard the plaintext key.finishCreateOrUpdateFilewithaccessLevel: "HOLDERS"(or"ADMIN") andencryptionMetadata: { encryptionSystem, encryptedDek, iv, contentHash, accessControlConditions, encryptedBy, encryptedAt }β echoencryptionSystemverbatim, never hardcode it.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
No
Bearerin front of amol_credential.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_FOUNDonce redeemed,NONCE_EXPIREDpast the window,INVALID_SIGNATUREif a later fetch superseded it. The fix is always a freshgetServiceSignInMessage, never a retry of the same signature.Success on a mutation is
error == nullβ never a truthy payload field, and never amessagestring.Read
error.detailswith the tolerantparseDetailsabove β never a bareJSON.parse. In-band it is a JSON string (currently doubly encoded); on thrown query errors it is already an object.Filter mint receipt logs to the LabNFT address before decoding
OclIdentityCreated.Send the presigned
PUTwith the returned headers unchanged, and the raw bytes as the body.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.A successful
createLabdoes not mean the lab is writable yet. Step 4's first call can returnNOT_FOUND("Project 0x⦠does not exist") for a few seconds, becausecreateLabfalls back to an onchain ownership check while the file mutations read the indexed record. RetryNOT_FOUNDwith backoff on the first write after a mint; do not re-runcreateLab, which then returnsCONFLICT.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 theaccountargument in access conditions). Putting an owner's address wherelabAccountAddressbelongs uploads fine and then locks everyone out of the file, with no error saying why.oclIdis none of them β it is a lab id whose trailing 40 hex chars happen to be the OCL account address. See the three wallets.Production has introspection off and a depth limit of 10. Generate types against staging.
Related
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
Last updated
Was this helpful?