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

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 PUBLIC upload skips the DEK request and encryptionMetadata entirely and is just the three-call initiateCreateOrUpdateFile β†’ PUT β†’ finishCreateOrUpdateFile flow 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.

  • viem and node-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 PUBLIC file, go straight to 5d with accessLevel: "PUBLIC" and omit encryptionMetadata β€” that's the whole upload. The DEK request, local AES-256-GCM encryption, and accessControlConditions below 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:

Constant
Staging (this walkthrough)
Production

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 chainId in Step 4 is read as CHAIN.id (8453 for base, 84532 for baseSepolia) β€” it tracks CHAIN and 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, no Bearer prefix) and the self-issued X-Service-Token from 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 base spends real ETH from the wallet behind WALLET_PRIVATE_KEY, and the assignment agreement you sign is a real one. Test the full flow on staging first.

  • SERVICE_NAME should 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