charli3-js wraps two rounds of on-chain activity that together make up a Charli3 price update. Knowing what each round does makes the API obvious.
The pull-oracle model
Nobody pre-posts prices. Whenever an app needs a fresh price, it pays a small fee to post one. Other readers then use that price for free until it expires (5 minutes on preprod). Cheap to read, honest to use: the chain shows exactly who signed what and when.Round 1: signed feeds from each node
Each oracle node publishes a signed price message. It contains:- The pair (like
ADA/USD) - A price value with 6 decimal places
- A validity window (start slot, end slot)
- An Ed25519 signature from the node’s key
Round 2: one on-chain tx
Round 2 combines those signed messages into one Cardano transaction:Pick a price
The SDK throws out outliers (IQR, the same trick a statistics class teaches). The middle value of what is left becomes the on-chain price.
Build the message
It builds a CBOR message with the feeds used, the median price, the validity window, and how to split the fee among the nodes that contributed.
Build the tx
Using Lucid Evolution, the SDK spends the current oracle UTXO and produces a new one with the new price datum. It attaches the reference script UTXO so the validator can read it.
Collect signatures
The validator wants signatures from the nodes that contributed. The SDK asks each node to sign the tx body, then adds those signatures to the tx.
getOdvReference("ADA/USD") and see the new price.
The whole loop, side by side
What ships in the SDK
Presets (src/config/presets.ts)
Presets (src/config/presets.ts)
All the preprod and mainnet values: oracle addresses, policy IDs, reference script UTXO, feed list, node URLs and keys, validity length, Kupo endpoint. If Charli3 ever changes them, you get the new values with
npm update charli3-js.Chain layer (src/chain/kupo.ts)
Chain layer (src/chain/kupo.ts)
A small Kupo client for reading UTXOs and datums. You can swap it by passing your own
kupoUrl.Oracle reader (src/oracle/reader.ts)
Oracle reader (src/oracle/reader.ts)
Finds the oracle UTXO, reads the datum, returns price + freshness + UTXO.
Node client (src/oracle/client.ts)
Node client (src/oracle/client.ts)
Calls each oracle node over HTTP, checks signatures, returns the collected feeds.
Round 2 builder (src/odv/round2.ts)
Round 2 builder (src/odv/round2.ts)
Builds the Round 2 transaction. Handles the tricky bits the validator cares about (see below).
Signature client (src/odv/sign-client.ts)
Signature client (src/odv/sign-client.ts)
Asks the oracle nodes to sign the tx body, then packs those signatures into the format Lucid expects.
Two things that took real work
A lot of the effort was making the Round 2 tx actually pass validation. Two things were sneaky:CBOR map ordering. The validator hashes the message and compares bytes. Maps have to be built in the exact byte order the validator expects, or it rejects the tx with a hash mismatch. The SDK builds them that way.
Slot-rounded validity bounds. The validator sees slot numbers, not milliseconds. So the timestamps in the datum have to match the validator’s view after slot rounding, not what the wallet clock says. The SDK rounds the start down and the end up to the nearest slot before writing them to the datum.
What the SDK does not do
- It does not run an oracle node. It talks to the ones Charli3 runs.
- It does not hold keys. Signing happens in your Lucid wallet or in the oracle nodes.
- It does not hide fees. Round 2 costs ~1.5 tADA and you supply a wallet that can pay.
- It does not change the trust model. Same node keys, same validator as the Python SDK.