A Cardano pull-based oracle for shipment tracking
Shipping Oracle exposes signed, verifiable shipment status information to any Cardano smart contract. The architecture is inspired by Pyth: the oracle never submits transactions itself — it publishes a signed attestation over HTTP that consumers embed in their own transactions. On-chain, an Aiken withdrawal validator verifies the Ed25519 signature against a governance UTxO identified by a one-shot NFT.
Milestone 2 status: the pull-based architecture is the current design. The previous push-based (cron-polling) model has been retired. See
spec/001-milestone-2.mdfor the full implementation plan.
- Consumer asks the oracle:
GET /v1/shipment?carrier=usps&tracking_number=.... - Oracle fetches the current status from the carrier API (Shippo), hashes the carrier + tracking number so no PII is exposed on-chain, and signs the Plutus-canonical CBOR of
OracleData. - Oracle replies with
data,plaintext,signature,public_key, andcbor_hex. - Consumer builds a Cardano transaction that:
- adds the governance UTxO as a reference input (carries the oracle verification key),
- attaches the oracle validator via the withdrawal trick (0-lovelace withdrawal from the script's reward address),
- passes the signed
OracleRedeemerin the withdrawal redeemer.
- On-chain validator finds the governance UTxO via its unique NFT, reads
oracle_vk, recomputesserialise_data(OracleData)and runsverify_ed25519_signature.
Sources: diagrams/milestone-2-c4-container.puml, diagrams/milestone-2-sequence.puml. Regenerate PNGs with ./diagrams/build.sh (requires Docker; PNGs are committed so GitHub renders them without any build).
shipping-oracle/
├── backend/ # Rust HTTP oracle (axum + pallas + ed25519-dalek)
├── sdk/ # Consumer SDKs (Rust + TypeScript)
├── onchain/ # Aiken validators (governance_nft mint + oracle withdraw)
├── tx3/ # TX3 protocol (publish_scripts, bootstrap_governance, consume_oracle_data)
├── diagrams/ # PlantUML C4 + sequence diagrams (sources + PNGs)
└── spec/ # Numbered implementation specs
The Milestone 3 SDKs live under sdk/:
sdk/rust— Rust SDK.sdk/typescript— TypeScript SDK (TS/Node apps; verification is byte-identical to the Rust SDK and the Aiken validator).
Both wrap the oracle HTTP contract and give consumers a typed flow for:
- fetching
GET /v1/shipmentattestations - verifying the Ed25519 signature and canonical CBOR payload
- keeping application context such as
order_idlinked to the resulting shipment commitment
The Rust SDK additionally generates tx3-ready consume_oracle_data arguments.
A runnable end-to-end example — an off-chain keeper that drives a deployed Cardano escrow from this oracle's IN_TRANSIT / DELIVERED signals — lives in the tx3 e-commerce template. See its Oracle-Driven Escrow Settlement integration guide for the full flow, trust model, the IN_TRANSIT → mark_shipped / DELIVERED → release mapping, the buyer-initiated refund rationale, and how to run the keeper.
GET /v1/shipment?carrier=usps&tracking_number=ABC123
Response:
{
"data": {
"carrier_hash": "abc…",
"tracking_number_hash": "def…",
"status": "DELIVERED",
"timestamp": 1712000000
},
"plaintext": {
"carrier": "usps",
"tracking_number": "ABC123"
},
"signature": "hex…",
"public_key": "hex…",
"cbor_hex": "d8799f…ff"
}| Field | What it is | On-chain use |
|---|---|---|
data |
Hashed identifiers + status + timestamp | Contents of OracleData |
plaintext |
Original carrier / tracking number (UX only, never signed) | — |
signature |
Ed25519 over cbor_hex bytes |
Redeemer signature |
public_key |
Oracle verification key (32 bytes, matches GovernanceDatum.oracle_vk) |
Verified against governance UTxO |
cbor_hex |
Canonical CBOR of the PlutusData form of data — embed these bytes verbatim |
Redeemer data (raw) |
The status vocabulary is DELIVERED, NOT_DELIVERED, IN_TRANSIT, PRE_TRANSIT, UNKNOWN. Consumers decide what to do with non-final states (unlike the old model, which only surfaced final statuses).
Liveness probe, returns { "status": "ok" }.
OracleData is serialised as PlutusData::Constr(0, [carrier_hash, tracking_number_hash, status, timestamp]) with an indefinite-length field array — byte-identical between pallas::codec::minicbor and Aiken's builtin.serialise_data. This alignment is the #1 technical risk and is verified by backend/tests/cbor_alignment.rs and onchain/lib/cbor_alignment_tests.ak using three shared test vectors.
// onchain/lib/types.ak
type GovernanceDatum { oracle_vk: ByteArray }
type OracleData {
carrier_hash: ByteArray,
tracking_number_hash: ByteArray,
status: ByteArray,
timestamp: Int,
}
type OracleRedeemer {
data: OracleData,
signature: ByteArray,
}The governance UTxO (the UTxO whose datum holds oracle_vk) is identified by a unique token minted by a one-shot minting policy that requires consuming a specific seed UTxO. Anyone can send UTxOs to the oracle's address, but only one UTxO in the universe carries the governance NFT. Rotating the oracle key = move the NFT to a new UTxO.
# 1. On-chain: compile validators + run tests
cd onchain
aiken check # unit tests + CBOR alignment
aiken build # emit plutus.json
# 2. Backend: run unit + HTTP integration tests (no network access required,
# Shippo is stubbed via wiremock)
cd ../backend
cargo test # backend/tests/*.rs + backend/tests/cbor_alignment.rs
# 3. Backend: run the HTTP server (requires Shippo + Cardano env vars)
cp .env.example .env # fill ORACLE_SK, SHIPPO_API_KEY, TRP_URL, ...
cargo run
curl 'http://localhost:3000/v1/shipment?carrier=usps&tracking_number=...'
# 4. SDK: run the Rust SDK tests and examples
cd ../sdk/rust
cargo test --all-targets -- --nocapture| Variable | Purpose |
|---|---|
SHIPPO_API_KEY |
Shippo tracking API token |
ORACLE_SK |
Oracle Ed25519 signing key (32 bytes, hex) |
ORACLE_PKH |
Oracle verification key hash (28 bytes, hex) |
ORACLE_ADDRESS |
Cardano address the oracle controls |
TRP_URL |
TRP endpoint (used by consumers to resolve tx3 txs) |
LISTEN_ADDRESS |
HTTP bind address (optional, default 0.0.0.0:3000) |
TRP_API_KEY |
Optional — required for hosted TRPs |
Licensed under the Apache License, Version 2.0. See LICENSE.

