> For the complete documentation index, see [llms.txt](https://docs.molecule.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.molecule.xyz/api-reference/tokenization-api.md).

# Tokenization API

## Overview

The Tokenization API enables developers to generate Lab (OCL) membership agreements and tokenize Labs into IP Tokens (IPTs) on Base. It combines offchain GraphQL mutations with onchain blockchain transactions to create legally-bound, tradeable research assets.

**Capabilities:**

* Generate Lab (OCL) membership agreements
* Tokenize Labs into IP Tokens (IPTs) on Base
* Manage the complete tokenization lifecycle
* Integrate with smart contracts via viem/ethers

***

## Authentication

All Tokenization API mutations require a consumer credential.

### Obtaining a Consumer Credential

The consumer credential is the same one every Molecule API uses — if you already have one, you are ready. To request one, use the [credential request template](/api-reference/getting-started.md#1-a-mol-consumer-credential-the-one-manual-step) on our [Discord community](https://t.co/L0VEiy4Bjk), adding your expected tokenization volume.

You'll receive:

* **Consumer credential** (`mol_<consumerId>_<secret>`) for authentication
* **Technical Integration Guide** with complete code examples and ABI files (not yet published; requested with the credential)

### Using Your Consumer Credential

Include the consumer credential in all requests using the `Authorization` header, with no `Bearer` prefix:

```bash
Authorization: YOUR_CONSUMER_CREDENTIAL
```

***

## API Endpoint

```
Production: https://production.graphql.api.molecule.xyz/graphql
Staging:    https://staging.graphql.api.molecule.xyz/graphql
```

***

## OCL Membership Agreement

Generate the membership agreement for an onchain lab (OCL) — the terms document an IP Token holder accepts when the Lab is tokenized. It returns an `agreementKey` (an S3 object key) paired with a SHA-256 `agreementContentHash` that binds the signature to the exact document.

### generateOclMembershipAgreement

**Input:**

* `agreementData` (AWSJSON): Stringified object with the fields below.

**Required Fields in agreementData:**

```json
{
  "oclId": "0x0101000000000000000000000000000000000000000000000000000000000042",
  "symbol": "LAB-SYMBOL",
  "title": "Lab Membership Agreement"
}
```

Generates the OCL membership agreement document for a Lab token and stores it in the public OCL agreements bucket. The returned `agreementKey` and `agreementContentHash` are the inputs to `getOclTermsMessage`. Fails with INVALID\_METADATA when `agreementData` is not valid JSON, INVALID\_INPUT when it fails validation or `oclId` is malformed, and INTERNAL\_ERROR (retryable) when generation or storage fails.

Returns [`GenerateOclMembershipAgreementResult!`](/api-reference/types.md#generateoclmembershipagreementresult).

| Name            | Type       | Description                                                                                                                                                                                |
| --------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `agreementData` | `AWSJSON!` | JSON object with `oclId` (32-byte OCL id as `0x` plus 64 hex, any case), `symbol` (ticker, 1-20 alphanumeric characters) and optionally `title` (display name). Any other key is rejected. |

**Response:**

```json
{
  "agreementKey": "0x0101...0042/agreements/3f2a....json",
  "agreementUrl": "https://...",
  "agreementContentHash": "0xabcdef...",
  "agreementType": "OCL_MEMBERSHIP",
  "generatedAt": "2026-07-15T10:30:00.000Z",
  "isSuccess": true
}
```

**Example Request:**

```bash
curl -X POST https://production.graphql.api.molecule.xyz/graphql \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_CONSUMER_CREDENTIAL' \
  -d '{
    "query": "mutation GenerateOclMembershipAgreement($agreementData: AWSJSON!) { generateOclMembershipAgreement(agreementData: $agreementData) { agreementKey agreementUrl agreementContentHash agreementType generatedAt isSuccess error { message } } }",
    "variables": {
      "agreementData": "{\"oclId\":\"0x0101000000000000000000000000000000000000000000000000000000000042\",\"symbol\":\"LAB-SYM\",\"title\":\"Lab Membership Agreement\"}"
    }
  }'
```

> The returned `agreementKey` and `agreementContentHash` are consumed by the `getOclTermsMessage(agreementKey, contentHash, labId, chainId)` query to produce the message the member signs.

***

## Lab (OCL) Tokenization Workflow

Labs are tokenized on **Base** through the `OclTokenizer` contract. It mints one fractional ERC-20 **IP Token (IPT)** per Lab — deployed as a `LabToken` contract — gated by a signed membership agreement.

### High-Level Flow

```
Step 1: Generate Membership Agreement (API)
↓ Mutation: generateOclMembershipAgreement
↓ Returns: agreementKey (S3 key), agreementContentHash (SHA-256)

Step 2: Get OCL Terms Message (API)
↓ Query: getOclTermsMessage(agreementKey, contentHash, labId, chainId)
↓ Returns: the exact terms text to sign

Step 3: Sign Terms (CLIENT-SIDE)
↓ Plain personal_sign (EIP-191) over the message — not EIP-712
↓ ECDSA signatures and ERC-1271 smart-account signatures both accepted

Step 4: Tokenize (BLOCKCHAIN, Base)
↓ Smart contract: OclTokenizer.tokenize()
↓ Fee: Gas only
↓ Result: ERC-20 IP Token (LabToken) contract deployed ✓
```

The terms message is reconstructed onchain by `OclTermsPermissioner.specificTermsV1()` — the backend's `getOclTermsMessage` returns byte-identical text, so always sign exactly the string the API returns.

### OclTokenizer Contract Functions

**tokenize():**

* Creates the Lab's ERC-20 IP Token
* Parameters:
  * `labId` (uint256): The LabNFT token ID
  * `amount` (uint256): Initial supply issued to the caller (in wei)
  * `symbol` (string): Token ticker symbol — the name is derived automatically as `Lab Tokens of Lab #<labId>`
  * `s3Key` (string): `agreementKey` from Step 1
  * `contentHash` (bytes32): `agreementContentHash` from Step 1
  * `signature` (bytes): Signed terms from Step 3
* Caller must be the Lab's controller (the current LabNFT owner); reverts `MustControlLab()` otherwise
* One token per Lab — a second call reverts `AlreadyTokenized()`

**attachToken():**

* "Bring your own token": wraps a pre-existing ERC-20 (≤ 18 decimals) in a read-only `WrappedLabToken` carrying the Lab's metadata instead of minting a new one
* Parameters: `labId`, `s3Key`, `contentHash`, `signature`, `tokenContract`

**issue() / cap():**

* `issue(labToken, amount, receiver)` — mint additional supply; controller-only
* `cap(labToken)` — permanently freeze issuance; controller-only, irreversible

### Networks (Lab Tokenization)

| Network      | Chain ID | OclTokenizer (proxy)                         |
| ------------ | -------- | -------------------------------------------- |
| Base Mainnet | 8453     | `0x62F532C3f563D974deEc103AAb8cC597f4f9c84E` |
| Base Sepolia | 84532    | `0xEe19e0Db8a7e59538710FAF6ed3ab655BCfCdB24` |

***

## Requirements

### For Lab Tokenization

* **Consumer credential**: the same credential every Molecule API uses — see [Getting Started](/api-reference/getting-started.md#1-a-mol-consumer-credential-the-one-manual-step)
* **Lab Control**: The caller must be the Lab's controller (the current LabNFT owner)
* **Base ETH Balance**: Sufficient for gas fees on Base
* **Token Details**: Symbol and initial supply amount (the token name is derived automatically)

***

## Error Handling

All mutations follow a consistent error response format:

```json
{
  "isSuccess": false,
  "error": {
    "message": "Error description",
    "code": "ERROR_CODE",
    "retryable": true
  }
}
```

### Common Errors

| Error Code       | Description                            | Solution                                                                                           |
| ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| 401 Unauthorized | Missing or invalid consumer credential | Check the `Authorization` header — the `mol_` credential goes in directly, with no `Bearer` prefix |
| 400 Bad Request  | Invalid parameters or malformed JSON   | Verify input data format                                                                           |
| `INVALID_INPUT`  | Required fields missing or malformed   | Verify the input object shape                                                                      |

Separately, the smart contracts revert with their own errors — most commonly `AlreadyTokenized()` (each Lab can only be tokenized once) and `MustControlLab()` (only the Lab's controller can tokenize) from the `OclTokenizer`.

### Troubleshooting

**Signature rejected onchain:**

* Sign exactly the string returned by `getOclTermsMessage` — the permissioner reconstructs the terms onchain and the texts must match byte-for-byte
* Use a plain `personal_sign` (EIP-191) signature, and sign with the Lab controller's wallet (or a smart account that validates via ERC-1271)

**Blockchain Transaction Failures:**

* Verify sufficient ETH balance for gas on Base
* Ensure using correct contract address for your network
* Wait for transaction confirmation before proceeding

***

## Complete End-to-End Examples

For complete code examples including:

* The full Lab tokenization flow with code
* Smart contract ABIs and interfaces
* Error handling and retry logic
* Safe multisig integration

Contract addresses, ABIs and interfaces are published in the [Contracts reference](/references/contracts.md) — [`OclTokenizer`](/references/contracts/tokenizer.md), [`IPToken`](/references/contracts/ipt.md), [`AccessResolver`](/references/contracts/accessresolver.md) — so no request is needed for those. For the remaining narrative material (Safe multisig integration, worked retry logic), ask on the [Molecule Discord](https://t.co/L0VEiy4Bjk) using the [same request template](/api-reference/getting-started.md#1-a-mol-consumer-credential-the-one-manual-step), naming the Technical Integration Guide instead of a credential.

### Basic Example Structure

```javascript
// 1. Generate the membership agreement
const agreement = await generateOclMembershipAgreement(agreementData);

// 2. Get the exact terms text to sign
const terms = await getOclTermsMessage(
  agreement.agreementKey, agreement.agreementContentHash, labId, chainId
);

// 3. Sign the terms (plain personal_sign)
const signature = await walletClient.signMessage({ message: terms.message });

// 4. Tokenize the Lab onchain (Base)
const txHash = await tokenize(labId, initialSupply, symbol,
  agreement.agreementKey, agreement.agreementContentHash, signature);

console.log('Lab tokenized!', txHash);
```

***

## Smart Contract Details

**Contract ABIs:**

* Available from the verified [OclTokenizer on BaseScan](https://basescan.org/address/0x62F532C3f563D974deEc103AAb8cC597f4f9c84E), or request them from the Molecule team as part of the Technical Integration Guide

***

## Best Practices

### Workflow Management

* Store intermediate results (agreement keys, signatures) securely
* Implement retry logic for failed API calls
* Wait for blockchain transaction confirmations
* Validate each step before proceeding to next

### Security

* Never expose consumer credentials in client-side code
* Use environment variables for credentials
* Implement proper wallet key management
* Verify all signatures and authorizations

### Testing

* Use Base Sepolia for development
* Test the complete workflow end-to-end
* Verify the agreement document and terms text before signing

***

## Getting Support

For assistance with the Tokenization API:

* **Smart contracts**: addresses, ABIs and interfaces are published in the [Contracts reference](/references/contracts.md) — no request needed
* **Consumer credential**: the [request template](/api-reference/getting-started.md#1-a-mol-consumer-credential-the-one-manual-step) on Getting Started
* **Technical Integration Guide**: ask on Discord with that same template, naming the guide instead of a credential
* **Discord**: join our [community](https://t.co/L0VEiy4Bjk) for support

***

*Last updated: July 2026*
