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

Upload an encrypted file

Encrypt a file locally with AES-256-GCM, gate decryption on live onchain roles, and verify with a full decrypt round trip.

Same lab, same three-call upload β€” but the bytes are AES-256-GCM encrypted locally before they leave your machine, and decryption is gated on live onchain state. Encryption is a first-class flow, not an appendix: reach for it whenever the file is confidential and access should follow the lab's roles.

Steps 1–3 are identical to Create a lab and upload a public file β€” get a service token, mint, register. Pick up here with oclId, labAccountAddress, account and serviceToken already in hand (or with any lab you already hold a role on).

Conceptually: the backend hands you a one-shot data encryption key (DEK) in two forms β€” plaintext, and wrapped by the key custodian. You encrypt with the plaintext copy, throw it away, and store the wrapped copy in the file's metadata alongside the conditions under which the custodian may unwrap it again. Full model: Data Privacy & Access.

Before you start: you need the two prerequisites β€” a mol_ consumer credential and a funded Base Sepolia wallet β€” plus the shared setup block, which defines the config constants and the graphql() / assertOk() helpers every snippet below uses. The complete script at the end of this page carries all of it inline and runs standalone. Unfamiliar with a term used here? See the Glossary.

Step 4a: Get a DEK

const dekResult = await graphql(`
  mutation {
    generateDataEncryptionKey {
      plaintextDEK
      encryptedDek
      encryptionSystem
      error { code message requestId retryable details }
    }
  }
`);
assertOk(dekResult.generateDataEncryptionKey, "generateDataEncryptionKey");
const { plaintextDEK, encryptedDek, encryptionSystem } = dekResult.generateDataEncryptionKey;

Expected response:

encryptionSystem is backend-set β€” echo it verbatim in Step 4d, never hardcode "kms". Key custody is on a path to threshold cryptography, and echoing the value is what keeps your integration working across that change.

Requires authentication (service token or Privy session), so Step 1 must have run.

Step 4b: Encrypt locally

The plaintext DEK is now unreferenced; don't log it, don't persist it, don't send it anywhere. Web Crypto's AES-GCM appends the 16-byte authentication tag to the ciphertext, which is what the decrypt side expects.

Step 4c: Write the access conditions

accessControlConditions is a JSON-stringified array of predicates the backend re-evaluates against live chain state every time someone asks to decrypt. Two recipes cover almost everything.

Owner only β€” only the LabNFT owner (and authorised signers of its Token Bound Account, so a Safe's signers resolve through) can decrypt:

Owner or Contributor or Viewer β€” the recipe to use when the lab has a team, and the one the contributor agent in Agent access needs. hasRole is hierarchical (ROLE_VIEWER = 1; a Contributor and the Owner both pass a Viewer check), and the explicit isAuthorizedSignerForTba branch keeps the Owner covered even if conditions are ever evaluated against a non-canonical chain:

labAccountAddress here is the Lab's own OCL account, not the owner's wallet β€” passing the owner's address instead evaluates to false and silently locks everyone out of the file (the three wallets). :userAddress is substituted with the authenticated caller's wallet at evaluation time. Pass "2" instead of "1" to exclude Viewers. Evaluation walks the array left to right and short-circuits (or stops at the first true); any RPC error fails closed and the DEK is not released. The full condition grammar, including EvmBasicCondition, is on Data Privacy & Access.

Step 4d: Upload the ciphertext

The same three calls as the public upload, with the ciphertext in place of the raw file and encryptionMetadata attached on finish.

If it fails:

error.code

What happened

Fix

VALIDATION_FAILED, reason: INVALID_CONDITIONS

accessControlConditions isn't a valid stringified condition array

It is a JSON string, not an object. Check functionAbi is complete and returnValueTest present

VALIDATION_FAILED, reason: INVALID_ACCESS_LEVEL

PUBLIC on an encrypted file

Use HOLDERS or ADMIN

UNAUTHORIZED on generateDataEncryptionKey

No write role on the lab

Owner or Contributor required

UPSTREAM_UNAVAILABLE, "Path is occupied" on finish

A file already exists at that path β€” usually a re-run against the same lab

Not retryable despite the code. Pick a new path, or send ref (the previous datasetId) instead to add a version

NOT_FOUND on the first call after a mint

The mint is not indexed yet, even though createLab succeeded

Step 5: Verify by decrypting it

The real test is a round trip: ask the backend to unwrap the DEK, decrypt, and compare hashes. decryptDataKey re-evaluates the file's conditions against live chain state and your token's wallet, so a success here proves the gate works.

Expected: the hashes match and recovered equals your original file byte-for-byte.

If it fails:

error.code

What happened

Fix

UNAUTHORIZED

Your wallet does not satisfy the file's conditions

Check the role with the public listLabMembers(oclId) query. After a fresh grant, allow for indexer/chain lag and retry

FAILED_PRECONDITION, reason: LEGACY_ENCRYPTION

The file predates onchain-verified envelope encryption

Not decryptable through this mutation; use the original encryption client

FAILED_PRECONDITION, reason: NOT_ENCRYPTED / MISSING_DEK

The file has no encryptionMetadata

You're pointing at a public file

UPSTREAM_UNAVAILABLE

Condition evaluation could not reach the chain RPC

Fails closed by design. retryable: true

Web Crypto throws OperationError

Wrong IV, or ciphertext truncated

Use the iv returned by decryptDataKey, and pass the whole downloaded body including the trailing GCM tag

Complete script

Steps 1–3 are verbatim from Create a lab and upload a public file; this script carries them so it runs standalone.

Usage:


Next

Let an agent decrypt and contribute too

Run it against mainnet

How conditions are evaluated, in depth

Need help?

Ask the team on Discord. Reporting an API error? Include the requestId from the failing response so we can trace it.

Ask on Discord

Page wrong, unclear or missing something? Let us know with Was this helpful? on this page.

Last updated

Was this helpful?