Lesson 6 of 6

Add Metadata to a Transaction

Cardano transactions can carry arbitrary key-value data alongside the movement of ADA. This is transaction metadata: labels, notes, application-level identifiers, off-chain document hashes — anything a downstream service wants to read from the chain without interpreting validator logic.

Metadata is cheap, standardised, and indexed by every explorer and chain follower. In this lesson you'll attach a CIP-20 style "message" to a simple ADA transfer using Apollo's SetShelleyMetadata.


How Metadata Is Structured

Cardano ledger defines metadata as a map from label (unsigned integer key, sometimes called a "tag") to value (numbers, byte strings, text strings, lists, or maps of the same).

Conventionally, specific label numbers are reserved for specific uses. A small sample:

Label

Convention

Source

674

Transaction messages (free text note)

CIP-20

721

NFT asset metadata

CIP-25

61284

DRep metadata

CIP-119

1968

Pool metadata references

Long-standing convention

For this lesson we'll use 674 (CIP-20 messages) because it's the simplest: a single msg field containing an array of text strings.

The canonical shape for a CIP-20 message is:

{
  "674": {
    "msg": [
      "Hello from the course"
    ]
  }
}

That's what we want to put on-chain.


Apollo's Metadata Types

Apollo exposes metadata through github.com/Salvionied/apollo/serialization/Metadata. The types you'll touch:

type Metadata map[int]any              // label → value
type ShelleyMaryMetadata struct {      // the top-level wrapper we attach
    Metadata      Metadata
    NativeScripts []NativeScript.NativeScript // optional
}

Metadata is just map[int]any. The any on the value side means you can nest map[string]any, []any, int, []byte, string — matching CBOR's major types.

The builder method is SetShelleyMetadata(Metadata.ShelleyMaryMetadata) *Apollo.


Prerequisites

  • Completed 102.2 (working Apollo ADA transfer)
  • Familiarity with the Apollo builder pattern

Step 1: Import the Metadata Package

Add the import to your 102.2 code:

import (
    // ... existing imports
    "github.com/Salvionied/apollo/serialization/Metadata"
)

Step 2: Construct the Metadata Value

Before .Complete() in your builder chain, build the metadata and attach it:

meta := Metadata.ShelleyMaryMetadata{
    Metadata: Metadata.Metadata{
        674: map[string]any{
            "msg": []any{
                "Hello from the course",
            },
        },
    },
}

apollob, _, err = apollob.
    AddLoadedUTxOs(utxos...).
    PayToAddressBech32("your address here", 1_000_000).
    SetShelleyMetadata(meta).
    Complete()

Complete() returns (*Apollo, []byte, error). The middle []byte is a CBOR preview of the built body — safe to discard with _ in most flows, as we do in 102.2.

A few things to notice:

  • The outer value for label 674 is a map[string]any. This matches the CIP-20 shape: {"msg": [...]}. Apollo (via fxamacker/cbor) will serialise the map with string keys, which is what explorers expect.
  • The value of msg is []any{"Hello from the course"}, not []string{...}. Use any so mixed types work in future — the library CBOR-encodes based on runtime type.
  • SetShelleyMetadata slots in anywhere before .Complete(). Order doesn't matter relative to other builder calls.

Step 3: Sign, Submit, Inspect

Same flow as 102.2:

apollob = apollob.Sign()
tx := apollob.GetTx()
// submit via bfc.SubmitTx(*tx) or the tx-submission program from 102.4

Once the transaction lands, open it on Cardanoscan preprod and scroll to the Metadata section. You should see:

Label 674
{
  "msg": ["Hello from the course"]
}

Reading Metadata in Go

If your service needs to read metadata off the chain — for example, filtering transactions that carry a specific label — the TransactionEvent you get from Adder (Module 201) carries a Metadata lcommon.TransactionMetadatum field. You decode it by asserting against lcommon.TransactionMetadatum types and walking the map. We'll do this in Module 204.

For now, know that what you put in with SetShelleyMetadata is exactly what comes out on the other end — the round-trip is lossless.


Common Pitfalls

Putting a string key on a numeric label

The top-level map is map[int]any, keyed by label number. It must be an int, not a string. This is a fast fail at compile time:

// wrong — won't compile
Metadata.Metadata{"674": ...}

// right
Metadata.Metadata{674: ...}

Strings longer than 64 bytes

Cardano metadata text strings are limited to 64 bytes each. CIP-20 handles longer messages by splitting across multiple elements in the msg array. Apollo will return an error at Complete() time if you exceed the limit on any single string. Split long messages explicitly:

"msg": []any{
    "First 64 bytes of the long message...",
    "Next 64 bytes of the long message...",
}

Binary data

For byte payloads, use []byte(...) not string(...). CBOR distinguishes text strings (major type 3) from byte strings (major type 2). Wallets and explorers render them differently.

Label collisions

Labels are a shared namespace. Pick a number that's either reserved for your use case (see CIPs) or clearly in unused territory (large arbitrary numbers like 45102025). Don't silently reuse 721 unless you actually mean CIP-25 NFT metadata.


You'll Know You're Successful When

  • Your transaction lands on preprod with the metadata visible on Cardanoscan
  • The label you chose matches what you set (e.g. 674)
  • The structure matches CIP-20's {"msg": [...]} shape
  • Adding SetShelleyMetadata didn't change the cost or success path of the transaction in any other way

Practice Tasks

  • Build a transaction with two metadata labels — 674 for the message and, say, 45102025 for a custom application ID. Verify both show up on the explorer.
  • Try submitting a transaction with a single 200-character string under msg. Observe what happens. Fix it by splitting.
  • Read CIP-20 and CIP-25 side by side. In one sentence each, describe when you'd use each label.

What's Next

Module 102 is complete. You can now build, sign, submit, bound in time, and annotate transactions. Next up:

  • Module 201 — watch transactions (including your own metadata) flow past an indexer
  • Module 204 — decode this metadata back out of CBOR programmatically