In 102.2, your transaction was valid as soon as you submitted it, and it stayed valid until the ledger no longer accepted it for some other reason.
In this lesson, you will add one small rule to that same transaction:
This transaction may be included only from slot X until slot Y.
Apollo handles this with two builder calls:
SetValidityStart(validFrom)
SetTtl(validUntil)
That is the whole code idea. The only extra work is choosing validFrom and validUntil from the current chain slot.
What the Two Numbers Mean
Cardano measures transaction validity with slot numbers. On preprod, slots advance about once per second.
Builder call
Meaning
Plain English
SetValidityStart(slot)
The transaction is invalid before this slot
"not before slot X"
SetTtl(slot)
The transaction is invalid at or after this slot
"expires at slot Y"
For a five-minute window, use the current slot as the start and add 300 slots for the end:
validFrom := int64(currentSlot)
validUntil := validFrom + 300
Prerequisites
- Completed 102.2 with a working Apollo transaction
- A Blockfrost API key for preprod
- A funded preprod wallet
You do not need to run Dolos or query a local node for this lesson. The Blockfrost chain context from 102.2 can read the latest block slot.
Step 1: Start From Your 102.2 Program
Open the tx.go file you built in 102.2.
Keep the setup the same:
- Load environment variables
- Create the Blockfrost chain context
- Load the wallet
- Fetch UTxOs
- Build, sign, and submit the transaction
The validity window belongs in step 5, right before Complete().
Step 2: Read the Current Slot
After you fetch UTxOs and before you build the transaction, ask the same Blockfrost context for the latest block slot:
currentSlot, err := bfc.LastBlockSlot()
if err != nil {
log.Fatal(err)
}
validFrom := int64(currentSlot)
validUntil := validFrom + 300 // about 5 minutes on preprod
LastBlockSlot() returns the slot from the latest block Blockfrost knows about. Apollo's validity methods expect int64, so convert the value before passing it into the builder.
Step 3: Add the Window Before Complete()
In 102.2, the builder line looked like this:
apollob, _, err = apollob.
AddLoadedUTxOs(utxos...).
PayToAddressBech32(recipient, 1_000_000).
Complete()
Add SetValidityStart(...) and SetTtl(...) before Complete():
apollob, _, err = apollob.
AddLoadedUTxOs(utxos...).
PayToAddressBech32(recipient, 1_000_000).
SetValidityStart(validFrom).
SetTtl(validUntil).
Complete()
if err != nil {
log.Fatal(err)
}
Read that chain as a sentence:
Spend these UTxOs, pay this address, make the transaction valid from the current slot, expire it five minutes later, then complete the transaction.
Complete() must come after the validity calls because Apollo uses the final transaction body when it balances the transaction and calculates the fee.
Step 4: Sign and Submit
The signing and submission code does not change:
apollob = apollob.Sign()
tx := apollob.GetTx()
txID, err := bfc.SubmitTx(*tx)
if err != nil {
log.Fatal(err)
}
Submit within five minutes. If the transaction reaches the node after validUntil, the node rejects it with an OutsideValidityInterval error.
Full Changed Section
This is the part of tx.go that changes from 102.2:
utxos, err := bfc.Utxos(*apollob.GetWallet().GetAddress())
if err != nil {
log.Fatal(err)
}
currentSlot, err := bfc.LastBlockSlot()
if err != nil {
log.Fatal(err)
}
validFrom := int64(currentSlot)
validUntil := validFrom + 300
apollob, _, err = apollob.
AddLoadedUTxOs(utxos...).
PayToAddressBech32(recipient, 1_000_000).
SetValidityStart(validFrom).
SetTtl(validUntil).
Complete()
if err != nil {
log.Fatal(err)
}
Everything before and after this section can stay the same as the 102.2 example.
Why Use a Validity Window?
The most common reason is to stop an old transaction from lingering forever. Fees, UTxOs, and business rules can become stale. A TTL says, "if this does not land soon, let it fail instead of surprising me later."
The start slot is useful when a transaction must not be accepted too early. Examples include time-locked spending, launch windows, auction bids, and coordinated signing flows.
Most simple wallet transactions only need SetTtl(...). This lesson uses both ends so you can see the full validity interval.
Common Pitfalls
Setting the TTL too close
SetTtl(validFrom + 5) gives the network only a few seconds. The transaction may expire before a block producer includes it. Use 300 slots for interactive examples.
Passing a timestamp instead of a slot
Apollo expects absolute slot numbers, not Unix timestamps and not epoch numbers. Get the slot from the chain context.
Setting the window after Complete()
The validity interval is part of the transaction body. Set it before Complete().
Reusing an old transaction
If you build a transaction and wait too long, the TTL passes. Build a fresh transaction with a fresh slot instead of resubmitting the old one.
You'll Know You're Successful When
- Your transaction submits successfully while the window is open
- If you deliberately set
SetTtl(validFrom - 100), submission fails withOutsideValidityInterval - You can explain
SetValidityStartas "not before" andSetTtlas "expires at"
Practice Tasks
- Build a transaction with
SetTtl(validFrom + 5)and submit it. Did it land before it expired? - Build a transaction with
SetValidityStart(validFrom + 600). Try to submit it immediately and inspect the error. - Remove
SetValidityStart(...)but keepSetTtl(...). Explain why this is enough for many basic transactions.
What's Next
- 102.6 — attach metadata to a transaction so off-chain services can tag and query it
- Module 201 — watch validity intervals in action as transactions stream past