Example Workflow: Mint β Upload
A complete, runnable walkthrough that takes a wallet with no prior credentials and no onchain lab all the way to a file living in its dataroom: prove control of the wallet to mint a service token, mint the LabNFT, register the dataroom, sign the assignment agreement, then upload a file. Each step below links back to its full reference; the Complete Script at the end wires all five together.
Encryption is optional. Step 5 below shows the encrypted path since it's the more involved one to get right, but most files don't need it β a plain
PUBLICupload skips the DEK request andencryptionMetadataentirely and is just the three-callinitiateCreateOrUpdateFileβ PUT βfinishCreateOrUpdateFileflow from Files. Reach for encryption when the file is confidential and access should be gated by onchain role or ownership β see Data Privacy & Access.
This is the workflow an autonomous agent needs to run end-to-end without any browser-based user interaction or manually provisioned Service Token β the only thing it needs ahead of time is a consumer credential and a funded wallet. It's written against staging (Base Sepolia, testnet ETH) end to end; see Running in Production at the bottom for the values to swap.
Prerequisites
A funded EOA on Base Sepolia β get testnet ETH from a Base Sepolia faucet
A consumer credential β see Authentication. No pre-issued Service Token needed; the workflow mints its own in Step 1.
viemandnode-fetch(npm install viem node-fetch)
Every environment-specific value used below β the GraphQL endpoint, contract addresses, and the viem chain β lives in this one block. Swapping to production later is a matter of replacing this block with the table in Running in Production.
import { baseSepolia } from "viem/chains"; // production: `base`
// ---- Staging (Base Sepolia) config β see "Running in Production" to swap ----
const GRAPHQL_URL = "https://staging.graphql.api.molecule.xyz/graphql";
const CHAIN = baseSepolia;
const FACTORY_ADDRESS = "0xd629FE2310b4309a212495F10A47f8436dcEfD90"; // OnChainLabFactory
const LABNFT_ADDRESS = "0x13Ff210695fdb54A7F928ECcc28BC3486c05BB28"; // LabNFT (proxy)
const ACCESS_RESOLVER_ADDRESS = "0x5493F472602C87318EA5Eff753cDD593bf9bF559"; // AccessResolver
const ACCESS_CONDITION_CHAIN = "baseSepolia"; // the `chain` string inside accessControlConditions
const CONSUMER_CREDENTIAL = process.env.CONSUMER_CREDENTIAL; // mol_<id>_<secret> β no "Bearer" prefix
const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY;
// Set once Step 1 exchanges a wallet signature for a token. Every call after
// that automatically starts sending it; public queries (like Step 1's own
// sign-in-message lookup) work fine without it.
let serviceToken;
async function graphql(query, variables) {
// Authorization is always required. X-Service-Token is added once we have
// one β omit it entirely rather than sending an empty header.
const headers = { "Content-Type": "application/json", Authorization: CONSUMER_CREDENTIAL };
if (serviceToken) headers["X-Service-Token"] = serviceToken;
const res = await fetch(GRAPHQL_URL, {
method: "POST",
headers,
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await res.json();
// Queries report failure here: a top-level errors[] entry whose errorType is
// the catalogue code. Mutations report expected failures in-band instead (see
// assertOk); a top-level entry on a mutation means a transport/infrastructure
// failure or an invalid request document.
if (errors) throw new Error(JSON.stringify(errors));
return data;
}
// Mutations report failure in-band: `error` is null on success. Throw on a
// non-null `error` so a failed step stops the workflow with the catalogue
// `code` and the `requestId` to quote in a bug report.
function assertOk(result, op) {
if (result.error) {
// `details` is a JSON-encoded string; JSON.parse(result.error.details ?? "{}").reason
// carries the specific cause when there is one.
const { code, message, requestId } = result.error;
throw new Error(`${op} failed: ${code}: ${message} (requestId ${requestId})`);
}
return result;
}Step 1: Get a Service Token
Prove control of the wallet instead of waiting on a manually issued token β the self-service path for agents, bots, and CI/CD. Fetch the deterministic sign-in message, sign it as a plain wallet message (EIP-191 personal_sign β not typed data), then exchange the signature for a token. Full reference: Service Tokens β Obtaining Tokens.
generateServiceToken reports failure the same way as every other mutation: error is null on success, and on failure it carries the catalogue code while token, tokenId and expiresAt are null (message mirrors error.message). The returned token authorizes this wallet's onchain-resolved role for whatever lab it acts on; it isn't scoped to a single oclId up front.
Step 2: Mint the LabNFT
Mint onchain via OnChainLabFactory.mintAndCreateAccount and read oclId off the OclIdentityCreated event. Reuses account / publicClient / walletClient from Step 1 and FACTORY_ADDRESS / LABNFT_ADDRESS from the config block. Full detail β the fee call and how oclId is derived β is on Lab Management.
Step 3: Create the Lab
Register the Kamu-backed dataroom for the freshly-minted oclId. Full reference: Create Lab.
Step 4: Sign the Assignment Agreement
Fetch the populated agreement, sign the LegalAgreementAcceptance EIP-712 payload, then submit it. Full schema and a self-test vector: Legal Agreements β EIP-712 Envelope.
Step 5: Upload a File (Encrypted)
Skip 5aβ5c if you don't need encryption. For a
PUBLICfile, go straight to 5d withaccessLevel: "PUBLIC"and omitencryptionMetadataβ that's the whole upload. The DEK request, local AES-256-GCM encryption, andaccessControlConditionsbelow are only for files that must be access-gated.
Request a data encryption key, AES-256-GCM encrypt the file locally via Web Crypto, then run the standard three-step upload with encryptionMetadata attached. Uses ACCESS_RESOLVER_ADDRESS / ACCESS_CONDITION_CHAIN from the config block. Full reference: Files β Advanced: Encrypted File Upload and Data Privacy & Access.
Complete Script
All five steps combined into one file, against staging. Run with WALLET_PRIVATE_KEY and CONSUMER_CREDENTIAL set, and a file at the path passed on the command line β no pre-issued Service Token needed. See Running in Production below to point this at mainnet instead.
Usage:
Running in Production
Everything above runs against staging (Base Sepolia, testnet ETH). To run the same script against production, replace the six values in the config block β nothing else in the script changes, since every step reads from these constants:
GRAPHQL_URL
https://staging.graphql.api.molecule.xyz/graphql
https://production.graphql.api.molecule.xyz/graphql
CHAIN (viem import)
baseSepolia from viem/chains
base from viem/chains
FACTORY_ADDRESS
0xd629FE2310b4309a212495F10A47f8436dcEfD90
0xECdF4f05384056507485C90aeAb0a83268760D6E
LABNFT_ADDRESS
0x13Ff210695fdb54A7F928ECcc28BC3486c05BB28
0x9F96027eeAFb9ad5F2b5d7043B36Ee96B2EeBE92
ACCESS_RESOLVER_ADDRESS
0x5493F472602C87318EA5Eff753cDD593bf9bF559
0x89a14Be8f7824d4775053Edad0f2fA2d6767b72B
ACCESS_CONDITION_CHAIN
"baseSepolia"
"base"
A few things that follow automatically from that swap and don't need separate handling:
EIP-712
chainIdin Step 4 is read asCHAIN.id(8453forbase,84532forbaseSepolia) β it tracksCHAINand needs no separate edit. See Legal Agreements β EIP-712 Envelope for why this must match the LabNFT's actual deployment chain.Headers and the
graphql()helper are identical in both environments βAuthorization(consumer credential, noBearerprefix) and the self-issuedX-Service-Tokenfrom Step 1 work the same way against both endpoints. See Authentication.The
mintFeeWei()read in Step 2 already queries the live contract, so it picks up whatever fee production has configured without a code change.
What doesn't follow automatically, and is on you to handle:
Real funds. Minting on
basespends real ETH from the wallet behindWALLET_PRIVATE_KEY, and the assignment agreement you sign is a real one. Test the full flow on staging first.SERVICE_NAMEshould identify the real integration once you're not just testing β it's echoed into the sign-in message and stored against the issued token.Full deployment list, including every other OCL contract on both chains: Contracts reference.
Last updated