Operations for creating and administering a Lab: creating the dataroom, managing its LabNFT display metadata, managing members, and linking its decentralised identifier (DID).
Register a Kamu-backed lab (data room) for an onchain lab (OCL) that already exists onchain. The lab is identified by its canonical oclId (a 32-byte hex string, 0x-prefixed).
Prerequisite — the LabNFT must be minted first.
createLabdoes not mint anything; it attaches a dataroom to an OCL that already exists. Minting happens onchain viaOnChainLabFactory.mintAndCreateAccount, which mints the LabNFT and deploys its bound account in one transaction — see Lab Creation for the contract-level flow, or Molecule Labs for what a Lab is and howoclIdis derived from the minted token. If you'd rather not touch the contracts directly, the Molecule app does this for you in Step 1: Create Your Onchain Lab.
Admin Authorization Required: This mutation requires either a service token (JWT) from the Molecule team OR a valid Privy authentication token. The caller must be the LabNFT owner (or an authorized multisig signer) for the given
oclId.
GraphQL Mutation:
mutation CreateLab($oclId: String!) {
createLab(input: { oclId: $oclId }) {
isSuccess
message
error {
message
code
retryable
}
lab {
oclId
shortname
labAccountAddress
labNftTokenId
}
}
}Parameters:
The mutation takes a single CreateLabInput object:
| Field | Type | Required | Description |
|---|---|---|---|
| oclId | String | Yes | Canonical 32-byte oclId (lowercase 0x-hex) of the onchain lab |
Prerequisites:
- LabNFT Ownership: You must own the LabNFT for the
oclIdor be an authorized signer for it- For individual wallets: You must be the owner
- For multisig/Safe wallets: You must be one of the Safe owners
- For ERC-4337 accounts: You must be an authorized account owner
- Authentication: One of the following:
- Service Token (recommended for automation): Obtain from Molecule team via Discord
- Privy Token (for user-initiated requests): Use your authenticated Privy session
- LabNFT Must Be Minted: The onchain lab (LabNFT /
oclId) must already exist onchain before registering the lab
Authentication Options:
Option 1: Service Token (Recommended for Automation)
x-api-key: YOUR_API_KEY
X-Service-Token: YOUR_SERVICE_TOKENOption 2: Privy Token (User-Initiated)
x-api-key: YOUR_API_KEY
Authorization: Bearer YOUR_PRIVY_TOKEN
x-wallet-address: YOUR_WALLET_ADDRESSExample Request (Service Token):
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'X-Service-Token: YOUR_SERVICE_TOKEN' \
-d '{
"query": "mutation CreateLab($oclId: String!) { createLab(input: { oclId: $oclId }) { isSuccess message error { message code retryable } lab { oclId shortname labAccountAddress labNftTokenId } } }",
"variables": {
"oclId": "0x0101000000000000000000000000000000000000000000000000000000000042"
}
}'Success Response:
{
"data": {
"createLab": {
"isSuccess": true,
"message": "Lab created successfully",
"error": null,
"lab": {
"oclId": "0x0101000000000000000000000000000000000000000000000000000000000042",
"shortname": "apob-lab",
"labAccountAddress": "0x1234567890123456789012345678901234567890",
"labNftTokenId": "42"
}
}
}
}Error Responses:
Not Authorized (No Token):
{
"errors": [
{
"message": "Admin authorization required. Please contact Molecule team for service token access.",
"extensions": { "code": "UNAUTHORIZED" }
}
]
}Not the LabNFT Owner:
{
"data": {
"createLab": {
"isSuccess": false,
"message": "User is not authorized for this lab",
"error": {
"message": "Onchain verification failed: wallet address is not owner or authorized signer",
"code": "OWNERSHIP_VERIFICATION_FAILED",
"retryable": false
},
"lab": null
}
}
}Lab Already Exists:
{
"data": {
"createLab": {
"isSuccess": false,
"message": "Lab already exists for this oclId",
"error": {
"message": "A lab with this oclId already exists",
"code": "CONFLICT",
"retryable": false
},
"lab": null
}
}
}How It Works:
- Authentication Check: Validates service token or Privy token
- Onchain Verification: Verifies you own or are an authorized signer for the LabNFT (
oclId) - Lab Creation: Registers the Kamu-backed lab and its data room for the
oclId - Whitelist Update: Automatically adds your wallet address to the lab whitelist
- Returns Result: Lab details if successful, error details if failed
Use Cases:
- Automate Lab Creation: Register labs programmatically after minting LabNFTs
- CI/CD Integration: Automatically set up data rooms for new research labs
- Batch Operations: Register multiple labs for a portfolio of onchain labs
- User Self-Service: Allow users to create their own lab data rooms
Getting Service Token Access:
To obtain a service token for automated lab creation:
- Join our Discord community
- Contact the Molecule team
- Provide:
- Your wallet address
- Use case description
- Intended automation workflow
- You'll receive:
- API Key (for all APIs)
- Service Token (JWT for lab creation)
- Token expiration date
Retrieve complete details for a specific lab including all files. This is a public endpoint - no authentication required. Look up a lab by its oclId or, alternatively, by its human-readable shortname — provide exactly one.
🔓 Public Endpoint: The
labWithDataRoomAndFilesquery does not require authentication. You only need thex-api-keyheader - no Service Token is needed. File-level access control is handled via encryption rather than query-level authentication.
GraphQL Query:
query GetProject($oclId: String!) {
labWithDataRoomAndFiles(oclId: $oclId) {
oclId
shortname
trlValue
trlRationale
isVerified
dataRoom {
id
alias
files {
did
path
version
contentType
accessLevel
description
tags
categories
downloadUrl
}
}
}
}Example Request:
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"query": "query GetProject($oclId: String!) { labWithDataRoomAndFiles(oclId: $oclId) { oclId shortname dataRoom { id files { path contentType accessLevel tags } } } }",
"variables": {
"oclId": "0x0101000000000000000000000000000000000000000000000000000000000042"
}
}'The optional CMS-enriched fields
trlValue,trlRationale, andisVerified(see List All Projects) are also available on this query and are hydrated only when requested.
Partial update of the LabNFT display metadata (name, description, image, externalUrl). Omitted fields are left unchanged; an explicit null clears a field.
Authorization: Restricted to the OCL admin (LabNFT owner + multisig signers).
mutation UpdateLabNftMetadata(
$oclId: String!
$input: UpdateLabNftMetadataInput!
) {
updateLabNftMetadata(oclId: $oclId, input: $input) {
isSuccess
oclId
message
error {
message
code
retryable
}
}
}Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| oclId | String | Yes | Canonical 32-byte oclId of the lab |
| input | UpdateLabNftMetadataInput | Yes | Patch object (all fields optional) |
UpdateLabNftMetadataInput fields (all optional): name, description, image, externalUrl.
Generate a single-use presigned PUT URL to which the OCL admin uploads a LabNFT display image. contentType must be one of image/jpeg, image/png, image/webp, image/gif, or image/svg+xml. The public URL is patched onto the lab asynchronously by the image processor once the object lands in S3.
Authorization: Restricted to the OCL admin (LabNFT owner + multisig signers).
mutation GenerateLabImageUploadUrl($oclId: String!, $contentType: String!) {
generateLabImageUploadUrl(oclId: $oclId, contentType: $contentType) {
uploadUrl
key
expiresAt
isSuccess
error {
message
code
retryable
}
}
}Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| oclId | String | Yes | Canonical 32-byte oclId of the lab |
| contentType | String | Yes | Image MIME type (image/jpeg, image/png, image/webp, image/gif, image/svg+xml) |
Return the active members of a lab (owner, contributors, viewers), sourced from the indexed ocl_user table. Expired grants are excluded.
Public query — only an API Key is required. The same data is also exposed on the public
Lab/LabRef.membersfield.
query ListLabMembers($oclId: String!) {
listLabMembers(oclId: $oclId) {
isSuccess
message
members {
walletAddress
role
source
expiry
isAgent
grantedAt
}
error {
message
code
retryable
}
}
}Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| oclId | String | Yes | Canonical 32-byte oclId of the lab |
Member fields:
| Field | Type | Description |
|---|---|---|
| walletAddress | String | Lowercased wallet address of the member |
| role | LabMemberRole | Effective role: OWNER, CONTRIBUTOR, or VIEWER |
| source | LabMemberSource | Row that defines the membership: ONCHAIN_EVENT, MULTISIG_RESOLUTION, ACCESS_CONTRACT, or ACCESS_RESOLVER_EVENT |
| expiry | String | Unix-seconds expiry as a decimal string; null means the grant is permanent |
| isAgent | Boolean | True if the member is an agent identity (surfaced for UI; not used for authorization) |
| grantedAt | String | ISO-8601 timestamp the row was first persisted |
Public read-only snapshot of DID-linking state for an OCL. DID-linking runs automatically in the background after createLab; this query is for diagnostic and support visibility. No authentication required.
query GetDidLinkStatus($oclId: String!) {
getDidLinkStatus(oclId: $oclId) {
isSuccess
message
didLinkStatus {
oclId
status
userOpHash
txHash
accountDid
dataRoomDid
linkedDidCount
attempts
updatedAt
}
error {
message
code
retryable
}
}
}Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| oclId | String | Yes | Canonical 32-byte oclId of the lab |
status is a DidLinkingStatus: PENDING, SUBMITTED, LINKED, or FAILED (null before the first linking attempt). linkedDidCount reflects the number of active onchain DID links observed by the event indexer.