A Cardano oracle for tracking shipments
Shipping Oracle enables trustless, verifiable shipment status verification by combining on-chain smart contracts with off-chain oracle services. Customers create tracking requests on-chain, and an oracle service monitors real-world shipment status via carrier APIs, updating the blockchain when shipments reach final states.
- Track: A customer creates a tracking request by locking funds in a Tracking UTxO at the Oracle address.
- Poll: The backend fetcher continuously polls the Oracle address for tracking UTxOs and queries shipment status via Shippo API.
- Close: When a shipment reaches final status (
DELIVERED/NOT_DELIVERED), the oracle submits a close-shipment transaction, unlocking the tracking UTxO and sending the status result to the customer's outbox address.
shipping-oracle/
├── backend/ # Rust data fetcher (polls tracking UTxOs, queries Shippo, submits updates)
├── onchain/ # Aiken validator (validates tracking UTxO spends)
└── tx3/ # tx3 protocol definitions (publish, track_shipment, close_shipment)
Component Documentation:
- Backend Documentation - Data fetcher setup and operation
- On-chain Documentation - Validator logic and validation rules
- Tx3 Documentation - Protocol definitions and transactions
Scheduled Rust service that continuously monitors the Cardano blockchain for tracking UTxOs. For each tracking UTxO found, it queries the Shippo API to retrieve real-world shipment status. When a shipment reaches a final state (delivered or not delivered), the backend constructs and submits a close-shipment transaction to update the on-chain status and collect the oracle payment.
Key Responsibilities:
- Poll Oracle address for tracking UTxOs via Blockfrost
- Query Shippo API for shipment tracking status
- Determine if shipment status is final
- Build and submit close-shipment transactions via tx3/TRP
- Execute on configurable cron schedule
Aiken validator that governs tracking UTxO spends. Enforces critical rules to ensure tracking requests can only be closed by authorized oracles with valid shipment statuses.
Validation Rules:
- Transaction must have exactly two outputs (shipment + payment)
- Shipment output must go to the outbox address specified in TrackingDatum
- Shipment output must contain valid ShipmentDatum with matching carrier/tracking number
- Status must be either "DELIVERED" or "NOT_DELIVERED"
- Oracle must sign the transaction (verified via extra signatories)
- Payment output must send at least
tracking_pricelovelace to payment address - Payment output must have no datum attached
Protocol definitions written in tx3 that specify the structure and rules for all Cardano transactions in the system. Compiled into transaction intermediate representation (TIR) that can be resolved into concrete Cardano transactions via the TRP service.
Transactions:
publish: Publishes validator script on-chain as a reference scripttrack_shipment: Creates tracking UTxO with customer's carrier and tracking numberclose_shipment: Consumes tracking UTxO and emits shipment status result
The operator running the backend fetcher service. The oracle continuously monitors tracking UTxOs, queries real-world shipment status from carrier APIs, and submits close-shipment transactions when shipments reach final status. The oracle signs close-shipment transactions with their private key and receives payment for providing tracking updates.
User creating shipment tracking requests. The customer funds tracking UTxOs with ADA to initiate tracking, specifying the carrier, tracking number, and outbox address where they want to receive the final status result.
Destination address where the shipment status output (ShipmentDatum) is sent when a tracking request is closed. This address is specified by the customer in the TrackingDatum and controls where the final tracking result is delivered.
Address that receives the oracle's payment when a shipment is successfully closed. This address is configured in the validator and compensates the oracle for monitoring shipments and providing status updates. The payment comes from the funds originally locked in the tracking UTxO.
Creates a tracking request on-chain by locking funds at the Oracle address.
Purpose: Customer initiates shipment tracking by creating a tracking UTxO containing carrier and tracking number information.
Inputs:
- Customer funds UTxO containing sufficient ADA (to cover tracking price deposit + fees + change)
Outputs:
- Tracking UTxO (locked at Oracle address)
- Value:
TRACKING_PRICEADA + min UTxO requirement - Datum:
TrackingDatum
- Value:
- Change UTxO (returned to customer)
- Value: Remaining ADA after tracking deposit and fees
TrackingDatum:
type TrackingDatum {
carrier: ByteArray, // e.g., "usps", "fedex", "ups"
tracking_number: ByteArray, // Shipment tracking number
outbox_address: Address, // Where to send final status result
}Comments:
- The
TRACKING_PRICEvalue depends on the configured parameter on the on-chain validator. More information here.
---
config:
flowchart:
curve: stepBefore
---
flowchart LR
I1@{ shape: brace-r, label: "Customer wallet<br/>━━━━━━━━━━━━<br/>Address: Customer<br/>Value: (N + minADA) ADA" }
TX@{ label: "<br/><br/><br/><br/><br/><br/><br/><br/><br/> Track Shipment <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>"}
O1@{ shape: brace-l, label: "Tracking UTxO<br/>━━━━━━━━━━━━━<br/>Address: Oracle<br/>Value: (N + minADA) ADA<br/>Datum: TrackingDatum<br/>{ carrier, tracking_number, outbox_address}"}
O2@{ shape: brace-l, label: "Change UTxO<br/>━━━━━━━━━━━━━<br/>Address: Customer<br/>Value: K ADA"}
I1 --> TX
TX --> O1
TX --> O2
Note@{ shape: rounded, label: "where:<br/>- N: tracking price<br/>- K: remaining ADA after tracking deposit and fees" }
Oracle closes the tracking request by consuming the tracking UTxO and emitting the final shipment status.
Purpose: Consume tracking UTxO and emit shipment status result to the customer's outbox address while paying the oracle for their service.
Inputs:
- Tracking UTxO (from Oracle)
- Value:
TRACKING_PRICEADA + min UTxO - Datum:
TrackingDatum - Redeemer:
ConsumeTracking
- Value:
Outputs:
- Shipment UTxO (to outbox address)
- Value: min UTxO requirement (~2 ADA)
- Datum:
ShipmentDatum
- Payment UTxO (to payment address)
- Value:
TRACKING_PRICEADA (tracking deposit minus shipment UTxO value and fees) - Datum: None
- Value:
Redeemer:
type TrackingRedeemer {
ConsumeTracking // Signals intent to close tracking request
}ShipmentDatum:
type ShipmentDatum {
carrier: ByteArray, // Matches TrackingDatum.carrier
tracking_number: ByteArray, // Matches TrackingDatum.tracking_number
status: ByteArray, // "DELIVERED" or "NOT_DELIVERED"
timestamp: Int, // Unix timestamp when status was recorded
oracle_pkh: VerificationKeyHash, // Oracle's public key hash (for verification)
}Comments:
- The
TRACKING_PRICEvalue depends on the configured parameter on the on-chain validator. More information here.
---
config:
flowchart:
curve: stepBefore
---
flowchart LR
I1@{ shape: brace-r, label: "Tracking UTxO<br/>━━━━━━━━━━━━━━<br/>Address: Oracle<br/>Value: (N + minADA) ADA<br/>Datum: TrackingDatum<br/>{ carrier, tracking_number, outbox_address}"}
TX["`<br/><br/><br/><br/><br/>Close Shipment<br/>━━━━━━━━━━━━━━━━━<br/>**Validates**<br/>✓ Oracle signature present<br/>✓ Status is valid<br/>(_DELIVERED_ / _NOT_DELIVERED_)<br/>✓ Payment ≥ tracking_price<br/>✓ Shipment datum matches tracking<br/>✓ Two outputs only<br/><br/><br/><br/><br/><br/><br/>`"]
O1@{ shape: brace-l, label: "Shipment UTxO<br/>━━━━━━━━━━━━━━<br/>Address: Outbox<br/>Value: minADA ADA<br/>Datum: ShipmentDatum<br/>{carrier, tracking_number,<br/>status, timestamp, oracle_pkh}"}
O2@{ shape: brace-l, label: "Payment UTxO<br/>━━━━━━━━━━━━━━<br/>Address: Payment<br/>Value: N ADA<br/>Datum: None"}
I1 --ConsumeTracking--> TX
TX --> O1
TX --> O2
Note@{ shape: rounded, label: "where:<br/>- N: tracking price" }
All on-chain data types used across the protocol.
Attached to tracking UTxOs locked at the Oracle address. Contains the shipment information and destination for results.
type TrackingDatum {
carrier: ByteArray, // Carrier identifier (e.g., "usps", "fedex", "ups")
tracking_number: ByteArray, // Shipment tracking number from carrier
outbox_address: Address, // Cardano address to receive ShipmentDatum result
}Attached to shipment UTxOs sent to the outbox address. Contains the final tracking status and verification information.
type ShipmentDatum {
carrier: ByteArray, // Carrier identifier (must match TrackingDatum)
tracking_number: ByteArray, // Tracking number (must match TrackingDatum)
status: ByteArray, // Final status: "DELIVERED" or "NOT_DELIVERED"
timestamp: Int, // Unix timestamp when status was determined
oracle_pkh: VerificationKeyHash, // Public key hash of oracle that submitted update
}Used when spending tracking UTxOs from the Oracle.
type TrackingRedeemer {
ConsumeTracking // Single variant indicating tracking request closure
}Valid shipment status values enforced by the validator.
pub const status_delivered: ByteArray = "DELIVERED"
pub const status_not_delivered: ByteArray = "NOT_DELIVERED"Licensed under the Apache License, Version 2.0. See LICENSE.
