ODATANO on Cardano

@odatano/core is the OData V4 API for the Cardano blockchain. Blocks, transactions, addresses, UTxOs, pools and DReps are OData entities. ADA, multi-asset, mint and Plutus V3 transactions are OData actions that return unsigned CBOR. A wallet, a CLI key or an HSM signs, the service submits. Fiori, Excel, Power BI, any OData client and AI agents call the same routes.

It runs hosted at api.odatano.dev, standalone from the repo or Docker image, or as a plugin in your SAP CAP app. Needs Node.js 22.5 or newer and @sap/cds 10.

Services

ServicePathWhat
CardanoODataService/odata/v4/cardano-odata/Read: 22 entities, 19 actions. Lazy indexing with TTL refresh
CardanoTransactionService/odata/v4/cardano-transaction/Build and submit: 6 build actions, submit, status, utilities
CardanoSignService/odata/v4/cardano-sign/Signing requests, signature verification, HSM signing
CardanoIndexerService/odata/v4/cardano-indexer/Chain crawler: sync state, reorg log, pause and resume. Off by default
CardanoWorkerService/odata/v4/cardano-worker/Server-side wallet jobs: build, sign, submit, confirm. Off by default
CardanoAgentService/odata/v4/cardano-agent/Scoped, budgeted bearer tokens for agents. Off by default

Every entity set takes $filter, $select, $expand, $orderby, $top, $skip and $count. Actions are POST with a JSON body. $metadata carries the full schema. All services require an authenticated user; getLiveness and VerifyDataSignature are public.

Read

TopicEntityActions
NetworkNetworkInformationGetNetworkInformation
Blocks, epochsBlocks, EpochsGetBlockByHash, GetLatestBlock, GetEpochByNumber, GetLatestEpoch
TransactionsTransactions, inputs, outputs, TransactionMetadataGetTransactionByHash, GetMetadataByTxHash, ParseTransactionCbor
AddressesAddresses, AddressUTxOs, AddressAssets, AddressTransactionsGetAddressByBech32, GetUTxOsByAddress, GetUTxOsByCredential, GetAssetsByAddress, GetLatestTransactionsByAddress
AssetsAssets, AssetHistoryGetAssetInfo, GetAssetHistory
Staking, governancePools, Accounts, Dreps, epoch snapshotsGetPoolById, GetAccountByStakeAddress, GetDrepById
ProtocolLedgerProtocolParametersGetLedgerProtocolParameters

Actions fetch from the chain on a cache miss and store the row. Entity sets read the cache, so a $filter over Transactions covers what was fetched before, or everything the crawler pre-synced.

GET  /odata/v4/cardano-odata/Blocks?$orderby=height desc&$top=1
GET  /odata/v4/cardano-odata/Transactions?$filter=slot gt 12345678&$top=10
GET  /odata/v4/cardano-odata/Transactions('<hash>')?$expand=inputs,outputs
POST /odata/v4/cardano-odata/GetUTxOsByAddress   {"address": "addr_test1..."}

Transactions: Build, Sign, Submit

The server never holds a private key. A build returns unsignedTxCbor, txBodyHash, fee and a buildId. The client signs. The server verifies the witnesses and submits.

Build, Sign, Submit Build, Sign, Submit

Build

ActionWhat it builds
BuildSimpleAdaTransactionADA transfer. Optional assets, inline datum, lock at a script address, CIP-33 reference script
BuildTransactionWithMetadataTransfer with transaction metadata, e.g. CIP-20 or a label-1447 anchor
BuildMultiAssetTransactionNative assets plus ADA to one recipient
BuildMintTransactionMint or burn under a Plutus V3 policy. Required signers, reference inputs, metadata
BuildPlutusSpendTransactionSpend a script UTxO: validator, redeemer, continuing datum, extra outputs, combined mint
SetCollateralMakes sure an ADA-only collateral UTxO exists

Every build takes senderAddress, recipientAddress and lovelaceAmount, plus the fields of its kind. Full parameter tables: Transaction Workflow on GitHub.

curl -X POST http://localhost:4004/odata/v4/cardano-transaction/BuildSimpleAdaTransaction \
  -H "Content-Type: application/json" \
  -d '{"senderAddress":"addr_test1...","recipientAddress":"addr_test1...","lovelaceAmount":10000000}'

Sign

MethodsignerTypeWhere the key lives
Cardano CLIcardano-clia .skey file, backend automation
CIP-30 walletbrowser-walletbrowser extension: Lace, Eternl, Yoroi
Hardware wallethardware-walletLedger or Trezor through a wallet adapter
HSMserver sidePKCS#11 device, the key never leaves the chip

External signing: Build* → CreateSigningRequest → client signs → SubmitVerifiedTransaction. The request moves pending → verified → submitted and expires after 30 minutes. VerifySignature checks CBOR, body hash, fee payer and required signers without submitting.

HSM signing: Build* → SignAndSubmitWithHsm. Needs hsm.enabled and a PKCS#11 module; failures at start are non-fatal and the HSM actions answer ODATANO_HSM_UNAVAILABLE.

Submit

ActionParams
SubmitTransactionbuildId, signedTxCbor
SubmitSignedTransactionsignedTxCbor, network
CheckSubmissionStatusbound on TransactionSubmissions(id)

A duplicate submit answers ODATANO_TX_ALREADY_SUBMITTED. DeriveScriptAddress and ExtractPaymentKeyHash are pure helpers without a network call. VerifyDataSignature checks a CIP-30 signData signature for wallet login and is public.

Plutus V3

Buildooor is the sole transaction builder and computes the script data hash for mint and spend by construction. Plutus V1 and V2 scripts are rejected at build time.

FlowActions
LockBuildSimpleAdaTransaction with lockOnScript: true and outputDatumJson
SpendSetCollateral, then BuildPlutusSpendTransaction with validatorScript, redeemerJson, scriptTxHash, scriptOutputIndex
MintBuildMintTransaction with mintingPolicyScript and mintActionsJson; negative quantities burn
State machinespend with lockOnScript: true and inlineDatumJson re-locks the next state; extraOutputsJson adds outputs
Spend and mintmintActionsJson and mintingPolicyScript on BuildPlutusSpendTransaction, one atomic transaction

Also supported: parameterised validators (scriptParamsJson), required signers (requiredSignersJson), CIP-31 reference inputs, CIP-33 reference scripts on every build action, validity windows (validityStartMs, validityEndMs), forced inputs and the __INPUT_IDX:<txHash>#<index>__ placeholder inside redeemers and datums. Script hex from Aiken or Plutus is passed as is, CBOR wrapper included.

Worked examples: TRACE (batch NFTs with a continuing datum) and QUANTIX (atomic mint against oracle feeds).

2.0 subsystems

Three optional services, each off by default and each with CAP events for in-process consumers.

  • Crawler (crawler.enabled): pre-syncs blocks and transactions from a start slot into the cache, with a sync cursor, reorg log and pause or resume. Chain-sync over Ogmios or pagination over Blockfrost and Koios.
  • Wallet worker (walletWorker.enabled): server-side wallets run build, sign, submit and confirm as one job. SubmitWalletJob takes the same payload as the matching build action. Keys never pass through the API.
  • Agent grants (agentGrants.enabled): an admin issues an odat_… token with an action allow list, an optional wallet, a daily budget and an expiry. A request with x-agent-token runs as that grant, never as the operator. HSM signing and admin actions are never grantable.

Run it

As a CAP plugin

{
  "cds": {
    "requires": {
      "[development]": { "auth": { "kind": "dummy" } },
      "db": { "kind": "sqlite", "credentials": { "url": "db.sqlite" } },
      "odatano-core": {
        "network": "preview",
        "backends": ["blockfrost", "koios"]
      }
    }
  }
}

Then cds deploy --to sqlite and cds watch. Secrets such as the Blockfrost key belong in .env. Upgrading a consumer from 1.x: run cds deploy once, 2.0 adds six tables.

Standalone

git clone https://github.com/ODATANO/ODATANO && cd ODATANO
npm ci
cp .env.example .env       # NETWORK, BACKENDS, BLOCKFROST_API_KEY
cds deploy --to sqlite
cds serve

docker compose up -d brings up a local cardano-node, Ogmios and ODATANO on preview. The release workflow publishes ghcr.io/odatano/odatano; the image runs with HTTP basic auth (ODATANO_HTTP_PASSWORD) and SQLite or PostgreSQL (ODATANO_DB_URL).

Configuration

Plugin keys sit under cds.requires.odatano-core. Standalone reads environment variables. A plugin key wins over the variable.

KeyEnvDefaultMeaning
networkNETWORKpreviewmainnet, preview or preprod
backendsBACKENDS["koios"]priority order; first is live, the rest historical fallback
blockfrostApiKeyBLOCKFROST_API_KEYrequired with blockfrost unless a custom backend is set
blockfrostCustomBackendBLOCKFROST_CUSTOM_BACKENDself-hosted Blockfrost-compatible node, e.g. Dolos MiniBF
koiosApiKeyKOIOS_API_KEYoptional Koios bearer
ogmiosUrlOGMIOS_URLws://localhost:1337required with ogmios
primaryTimeoutMs, fallbackTimeoutMsPRIMARY_TIMEOUT_MS, FALLBACK_TIMEOUT_MS30000, 60000per backend
indexTtlMsINDEX_TTL_MS3600000cache TTL for addresses and accounts
hsm.*, crawler.*, walletWorker.*, agentGrants.*HSM_*, CRAWLER_*, WALLET_WORKER_*, AGENT_GRANTS_*offthe optional subsystems

Backends: Ogmios answers live queries and submits, Blockfrost and Koios serve history. A circuit breaker moves a failing backend out of rotation and retries after a cooldown. Production runs NODE_ENV=production with HANA and XSUAA; the full key list and the deployment guides are on GitHub.

Errors

Every error is { code, message, target? }. Branch on code, not on the HTTP status.

CodeHTTPMeaning
ODATANO_INVALID_INPUT400bad address or hash, missing field, JSON over limit
ODATANO_NOT_FOUND404not on chain
ODATANO_INSUFFICIENT_FUNDS400the sender cannot cover the build
ODATANO_TX_VALIDATION_FAILED400witness, signature or CBOR invalid
ODATANO_SCRIPT_VALIDATION_FAILED400Plutus script failed
ODATANO_TX_ALREADY_SUBMITTED409in the mempool or on chain
ODATANO_PROVIDER_RATE_LIMITED429Blockfrost or Koios limit
ODATANO_PROVIDER_UNAVAILABLE503backend timeout or 5xx
ODATANO_HSM_UNAVAILABLE503HSM device or session missing

On GitHub

The repo holds the detail this page leaves out. github.com/ODATANO/ODATANO