Skip to main content

Carmentis Operator

The Carmentis Operator is a self-hosted server that sits between your applications and the Carmentis blockchain. Your application talks to the Operator over a simple, documented HTTP API; the Operator takes care of everything blockchain-specific: building and signing microblocks, publishing them to a node, tracking their confirmation through an indexer, and producing proofs.

┌─────────────┐  HTTP + API key  ┌──────────────┐   RPC    ┌────────────────┐
│ Your │ ───────────────► │ Operator │ ───────► │ Carmentis node │
│ application │ ◄─────────────── │ │ └────────────────┘
└─────────────┘ │ - wallets │ HTTP ┌────────────────┐
│ - API keys │ ───────► │ Indexer │
┌─────────────┐ wallet flow │ - database │ └────────────────┘
│ User wallet │ ◄──────────────► │ - admin UI │
└─────────────┘ └──────────────┘

Why use the Operator?​

Publishing data on a blockchain directly means handling keys, key derivation, microblock construction, signatures, gas, node communication and confirmation tracking. The Operator packages all of this behind a few endpoints.

You want to…The Operator gives you…
Anchor business data on-chainA single POST to create an anchor request, then a status endpoint to follow it
Keep private keys out of your applicationWallets managed (and encrypted at rest) by the Operator
Let end users approve what is published in their nameA wallet-approval flow ("anchor with wallet")
Control who can publish whatScoped, revocable API keys (per application, endpoint regex, gas limits, expiry)
Prove that data is authentic and on-chainProof, record and "is published" endpoints
Verify credentials and signaturesVerification endpoints for SD-JWT credentials/presentations and signatures
Administrate everything without writing codeA built-in web admin UI

Core concepts​

  • Organization: the legal/logical entity that owns applications on the Carmentis blockchain.

  • Application: an application registered on-chain under an organization. Anchored data is always published for an application (identified by its virtual blockchain id, applicationId).

  • Wallet: the key material the Operator uses to sign and pay for publications. Each wallet is configured with:

    • an RPC endpoint, the Carmentis node to publish to;
    • an indexer endpoint, used to read the chain state and confirm publications;
    • optionally an allowed-endpoints regex.

    Sensitive values (such as the wallet passphrase and API keys) are encrypted in the database.

  • API key: the credential your application sends to the Operator. A key can be bound to an application and/or a wallet, restricted to endpoints matching a regex, limited to a gas range, given an expiry date, and deactivated at any time.

  • Anchor request: a request to publish data on-chain. It is created by your application and tracked by the Operator until it is confirmed.

Anchor request lifecycle​

Every anchor request has a status, exposed by the API:

StatusMeaning
createdThe request exists; no interaction has happened yet.
initiatedThe user's wallet has started the approval process.
submittedThe microblock has been published to the node. Its presence on chain is not yet confirmed.
anchoredThe indexer has confirmed that the microblock is on chain.
cancelledThe request has been cancelled.
failedThe microblock was submitted but never showed up in the indexer within the configured delay.
created ──► initiated ──► submitted ──┬──► anchored
│ │ └──► failed
└────────────┴──► cancelled

Automatic confirmation​

A background job periodically looks at every submitted request and asks the wallet's indexer whether the microblock hash is known:

  • found → the request becomes anchored;
  • not found yet → it stays submitted and is checked again at the next run;
  • not found after the timeout → it becomes failed;
  • indexer unreachable → the request is left untouched and retried later.

Both the frequency and the timeout are configurable (see [operator.anchoring]). Your application should therefore poll the status endpoint and treat anchored as the final success state.

Quick start​

1. Prerequisites​

  • Node.js 22+ and pnpm (or Docker)
  • A reachable Carmentis node and indexer
  • Optionally PostgreSQL or MySQL (SQLite is used by default)

2. Run it​

With Docker

docker run --rm --name carmentis-operator \
-p 3000:3000 \
-v "$(pwd)/config.toml:/app/config.toml" \
ghcr.io/carmentis/operator

From source

git clone https://github.com/carmentis/operator.git
cd operator
pnpm install
cp example-config.toml config.toml
pnpm build
pnpm start:prod # or: pnpm start:dev

The API is served under http://localhost:3000/api/v1/…, the interactive Swagger documentation under /swagger, and the admin UI under /admin.

3. Initial setup​

  1. On first start, the Operator generates an admin initialization token (written to operator/admin-token.txt by default). Open /admin/setup and use it to create the first administrator.

  2. Further administrators can be added with invitation links from the admin UI.

  3. In the admin UI, create (or import):

    1. a wallet (with its RPC and indexer endpoints),
    2. an organization,
    3. an application, then publish them on-chain if they are new,
    4. an API key, optionally scoped to the application.

    The API key is displayed once at creation: copy it.

4. Anchor your first data​

Send the API key in the x-api-key header.

curl -X POST http://localhost:3000/api/v1/wallet/anchor \
-H "x-api-key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"applicationId": "6AC2A4EBFD08F34C2EF4432041313F83EB4C8AB9154FAEF61ABB833613DA1C10",
"chainStorageInDays": 360,
"gasPriceInAtomics": 200000,
"channels": [{ "name": "Public", "public": true }],
"actors": [{ "name": "Issuer" }],
"data": { "invoice_id": "INV-001", "amount": 1000, "customer": "ACME Corp" },
"channelAssignations": [{ "channelName": "Public", "fieldPath": "this.*" }],
"author": "Issuer"
}'
# → { "anchorRequestId": "..." }

Then follow the request:

curl -H "x-api-key: <your-api-key>" \
http://localhost:3000/api/v1/anchorRequest/<anchorRequestId>/status
# → { "status": "submitted", "virtualBlockchainId": "...", "submittedMicroblockHash": "..." }
# … a little later:
# → { "status": "anchored", ... }

To append to an existing ledger instead of creating a new one, pass its virtualBlockchainId in the body.

Anchoring modes​

ModeEndpointUse it when…
Operator-signedPOST /api/v1/wallet/anchor
POST /api/v1/wallet/{walletId}/anchor
Your own backend publishes data with the Operator's wallet; no user interaction.
Wallet-approvedPOST /api/v1/wallet/anchorWithWallet
POST /api/v1/wallet/{walletId}/anchorWithWallet
A user must review and endorse the publication from their own wallet. Pass the endorser actor name in the body.

The /wallet/… form without {walletId} uses the wallet associated with the API key or the application. All endpoints check that the API key may publish to the requested application and that the gas price is within the key's limits.

API overview​

All public endpoints live under /api/v1. Authenticated endpoints expect the x-api-key header; the exact schemas are available in the Swagger UI (/swagger).

Anchor requests​

MethodPathDescription
GET/anchorRequestList anchor requests.
GET/anchorRequest/{id}Get an anchor request.
GET/anchorRequest/{id}/statusStatus, virtual blockchain id and microblock hash.
GET/anchorRequest/{id}/publishedWhether the request is published on chain.
GET/anchorRequest/{id}/proof/authenticityAuthenticity proof (for submitted / anchored requests).
POST/anchorRequest/{id}/cancelCancel a request.
DELETE/anchorRequest/{id}Delete a request.

Wallet​

MethodPathDescription
GET/wallet/{walletId}/recordRead a record from a virtual blockchain.
GET/wallet/{walletId}/proof/authenticityAuthenticity proof for a virtual blockchain.
GET/crypto/wallet/{walletId}/signature/pkWallet public signature key.
GET/crypto/wallet/{walletId}/actor/signature/pkActor public signature key.
GET/crypto/wallet/{walletId}/actor/pke/pkActor public encryption key.
POST/crypto/wallet/…Sign a binary message or a JSON payload with the wallet key.
POST/crypto/wallet/{walletId}/json-signature/verify and siblingsVerify binary / JSON signatures against the wallet key.

Chain, crypto helpers and credentials​

MethodPathDescription
GET/chain/microblock/{hash}/published?nodeEndpoint=…Check that a microblock is published on a given node.
GET/chain/applicationLedger/{vbId}/state?nodeEndpoint=…Actors and channels of an application ledger.
GET/chain/applicationLedger/{vbId}/actor/{name}/pk?nodeEndpoint=…Public keys of an actor.
POST/crypto/signature/…, /crypto/signature/json-signature/verifyVerify binary / JSON signatures.
GET/crypto/challenge, /crypto/nonce, /crypto/uuidGenerate a challenge, a nonce or a UUID.
POST/vc/sdjwt/verify, /vp/sdjwt/verifyVerify an SD-JWT verifiable credential / presentation.

Health​

MethodPathDescription
GET/healthOperator health check.
GET/public/helloPublic liveness endpoint.

Admin UI​

The admin UI (/admin) is a server-rendered web interface to configure the Operator:

  • Wallets: create from a passphrase or seed, set RPC and indexer endpoints, view the on-chain account and token balance breakdown.
  • Organizations and Applications: create, import existing ones, discover those already known to the indexer, publish them on-chain.
  • API keys: create, edit, enable/disable and delete keys, with scoping (application, wallet, endpoint regex, gas range, expiry).
  • Users and invitations: manage administrators.
  • Authentication: login with a Carmentis Desk wallet challenge.

Configuration​

The Operator reads a TOML file. It uses the first file found among:

  1. the path in the CONFIG (or OPERATOR_CONFIG) environment variable;
  2. config.toml, operator-config.toml or config-operator.toml in the working directory.

Every option has a default, so a minimal setup needs almost nothing. The full template is example-config.toml.

[operator]
# port = 3000

# SQLite is used by default. For PostgreSQL:
# [operator.database.postgresql]
# user = "your_username"
# password = "your_password"
# database = "your_database_name"
# url = "localhost"
# port = 5432

Anchoring​

[operator.anchoring]
# Cron expression: how often submitted anchor requests are checked against the indexer.
checkCronExpression = "* * * * *" # default: every minute

# Seconds after which a submitted request still unknown to the indexer is marked `failed`.
submittedTimeoutSeconds = 600 # default: 10 minutes

Choose submittedTimeoutSeconds according to your own tolerance: too short and a slow indexer can cause false failed results; too long and failures are detected late.

Main options​

SectionPurpose
[operator]port, dev mode.
[operator.admin.*]JWT secret and validity, Carmentis Desk relay URL, invitation validity.
[operator.anchoring]Confirmation job frequency and failure timeout.
[operator.swagger]Path of the Swagger UI.
[operator.cors]Allowed origins and methods.
[operator.database.*]SQLite / PostgreSQL / MySQL, and at-rest encryption settings.
[operator.protocols.wap]Version of the Wallet Authentication Protocol.
[operator.paths]Home directory and location of the token, JWT secret and encryption key files.

Keep the generated secrets safe. If you let the Operator generate the database encryption key, back up db-encryption-key.txt: without it, encrypted values (wallet passphrases, API keys) cannot be read.

Security notes​

  • Treat API keys like passwords: scope them to a single application, restrict endpointRegex, bound the gas range, set an expiry, and revoke unused keys.
  • Run the Operator behind HTTPS (reverse proxy) and restrict cors.origin in production.
  • Wallet passphrases and API keys are encrypted in the database; protect the database and the encryption key.
  • Restrict network access to the admin UI (/admin).

Further resources​