Skip to main content
Every on-chain order carries a datum that encodes the order parameters. This page covers how to build each field.

Datum structure

import { Data } from "@lucid-evolution/lucid";

const LimitOrderConfigSchema = {
  title: "LimitOrderConfig",
  anyOf: [
    {
      title: "LimitOrderConfig",
      dataType: "constructor",
      index: 0,
      fields: [
        { dataType: "bytes", title: "tag" },
        {
          title: "redeemerAddress",
          anyOf: [
            {
              title: "Address",
              dataType: "constructor",
              index: 0,
              fields: [
                {
                  title: "paymentCredential",
                  anyOf: [
                    {
                      title: "VerificationKey",
                      dataType: "constructor",
                      index: 0,
                      fields: [{ dataType: "bytes" }],
                    },
                    {
                      title: "Script",
                      dataType: "constructor",
                      index: 1,
                      fields: [{ dataType: "bytes" }],
                    },
                  ],
                },
                {
                  title: "stakeCredential",
                  anyOf: [
                    {
                      title: "Some",
                      dataType: "constructor",
                      index: 0,
                      fields: [
                        {
                          anyOf: [
                            {
                              title: "Inline",
                              dataType: "constructor",
                              index: 0,
                              fields: [
                                {
                                  anyOf: [
                                    {
                                      title: "VerificationKey",
                                      dataType: "constructor",
                                      index: 0,
                                      fields: [{ dataType: "bytes" }],
                                    },
                                    {
                                      title: "Script",
                                      dataType: "constructor",
                                      index: 1,
                                      fields: [{ dataType: "bytes" }],
                                    },
                                  ],
                                },
                              ],
                            },
                            {
                              title: "Pointer",
                              dataType: "constructor",
                              index: 1,
                              fields: [
                                { dataType: "integer", title: "slotNumber" },
                                { dataType: "integer", title: "transactionIndex" },
                                { dataType: "integer", title: "certificateIndex" },
                              ],
                            },
                          ],
                        },
                      ],
                    },
                    {
                      title: "None",
                      dataType: "constructor",
                      index: 1,
                      fields: [],
                    },
                  ],
                },
              ],
            },
          ],
        },
        {
          title: "input",
          dataType: "list",
          items: [
            { title: "PolicyId", dataType: "bytes" },
            { title: "AssetName", dataType: "bytes" },
          ],
        },
        { dataType: "integer", title: "tradableInput" },
        { dataType: "integer", title: "feePerExStep" },
        {
          title: "output",
          dataType: "list",
          items: [
            { title: "PolicyId", dataType: "bytes" },
            { title: "AssetName", dataType: "bytes" },
          ],
        },
        {
          title: "basePrice",
          dataType: "list",
          items: [{ dataType: "integer" }, { dataType: "integer" }],
        },
        { title: "cancellationPkh", dataType: "bytes" },
        { dataType: "bytes", title: "beacon" },
      ],
    },
  ],
} as const;

type BuildDatumParams = {
  paymentKeyHash: string;
  stakeKeyHash: string;
  input: [string, string];
  tradableInput: bigint;
  output: [string, string];
  basePrice: [bigint, bigint];
  cancellationPkh: string;
  beacon: string;
  feePerExStep?: bigint;
  tag?: string;
};

const buildDatum = ({
  paymentKeyHash,
  stakeKeyHash,
  input,
  tradableInput,
  output,
  basePrice,
  cancellationPkh,
  beacon,
  feePerExStep = 600000n,
  tag = "09",
}: BuildDatumParams) => {
  const datum = Data.to(
    {
      tag,
      redeemerAddress: {
        paymentCredential: {
          VerificationKey: [paymentKeyHash],
        },
        stakeCredential: {
          Inline: [
            {
              VerificationKey: [stakeKeyHash],
            },
          ],
        },
      },
      input,
      tradableInput,
      feePerExStep,
      output,
      basePrice,
      cancellationPkh,
      beacon,
    },
    LimitOrderConfigSchema
  );
  return datum;
};

Field reference

FieldTypeDescription
tagbytes"09" in hex format
redeemerAddressconstrUser’s address for receiving funds after execution
inputlist[policyId, tokenName] - what the user is selling. ADA = ["", ""]
tradableInputintegerAmount of input asset in base units
feePerExStepinteger600000 (0.6 ADA)
outputlist[policyId, tokenName] - what the user wants to receive
basePricelist[numerator, denominator] - price ratio
cancellationPkhbytesUser’s payment key hash for order cancellation
beaconbytes20-byte order identifier

Price calculation

Price is the output / input ratio, stored as [numerator, denominator].
Buying token with ADA:
  • Input: ADA
  • Output: Token
  • Price: tokenAmount / adaAmount = [tokenAmount, adaAmount]
Example: Buy 100 tokens for 50 ADA -> [100, 50] = 2 tokens per ADA

Input amount

  • Buy order: tradableInput = ADA amount the user is willing to spend
  • Sell order: tradableInput = token amount the user is selling

Beacon calculation

The order beacon is a 20-byte unique identifier that prevents beacon collisions. Its primary purpose is to prevent beacon collisions - ensuring that every order placed on-chain has a globally unique identifier. Structure:
  • Byte 0: order type (0 = limit, 1 = market)
  • Bytes 1-19: first 19 bytes of a blake2b-224 hash
All numeric indices must be encoded as big-endian uint64 (8 bytes).

Algorithm

1

Create empty beacon

28 bytes of zeros: 0000...0000
2

Serialize datum with empty beacon

Convert the datum (with empty beacon) to CBOR format.
3

Hash the datum

Apply blake2b-224 to the serialized datum CBOR bytes.
4

Build beacon preimage

Concatenate in order:
  • Transaction hash (32 bytes) of one of the transaction inputs
  • Output index (8 bytes, big-endian uint64)
  • Order index (8 bytes, big-endian uint64)
  • Datum hash (28 bytes) from step 3
5

Calculate final beacon

Apply blake2b-224 to the preimage, take the first 19 bytes, and prefix with the order type byte (0 or 1). Result: 20 bytes.

Implementation

import { Data, fromHex, toHex, CML, } from "@lucid-evolution/lucid";
import { blake2b } from "hash-wasm";
import { Uint64BE } from "int64-buffer";

const EMPTY_BEACON = toHex(new Uint8Array(28).fill(0));

// Step 1: Build datum with empty beacon (same shape as order datum, beacon = EMPTY_BEACON), serialize with LimitOrderConfigSchema
const orderDatumFields = {
  // All order datum fields except beacon (tag, redeemerAddress, input, tradableInput, feePerExStep, output, basePrice, cancellationPkh)
  ...buildDatumInput,
};
const orderDatumFieldsWithEmptyBeacon = {
  ...orderDatumFields,
  beacon: EMPTY_BEACON,
};
const datumCborHex = Data.to(orderDatumFieldsWithEmptyBeacon, LimitOrderConfigSchema, {
  canonical: true,
});
const datumCborBytes = CML.PlutusData.from_cbor_hex(datumCborHex).to_cbor_bytes();

// Step 2: Hash datum (blake2b-224 over serialized CBOR bytes)
const datumHashHex = await blake2b(datumCborBytes, 224);
const datumHash = fromHex(datumHashHex);

// Step 3: Beacon preimage (txHash 32 bytes + outputIndex 8 BE + orderIndex 8 BE + datumHash 28 bytes)
const txHashBytes = fromHex(outputReference.txHash);
const beaconPreimage = new Uint8Array([
  ...txHashBytes,
  ...new Uint64BE(Number(outputReference.outputIndex)).toArray(),
  ...new Uint64BE(Number(orderIndex)).toArray(),
  ...datumHash,
]);

// Step 4: Hash and prefix with order type (first 19 bytes of blake2b-224)
const beaconHashHex = await blake2b(beaconPreimage, 224);
const beaconHashFirst19 = fromHex(beaconHashHex.slice(0, 38));
const beacon = toHex(
  new Uint8Array([
    orderType === "limit" ? 0 : 1,
    ...beaconHashFirst19,
  ])
);
Where:
  • outputReference - one of the transaction inputs (UTxO reference)
  • orderIndex - index of the order output in the transaction (BigInt)
  • orderType - 'limit' or 'market'

Real example

Buy order: 50 ADA for NIGHT token at ~4.2 tokens per ADA. View transaction on CardanoScan