In 102.2 and 102.3 you submitted transactions through Blockfrost — either via Apollo's bfc.SubmitTx(...) or by pasting CBOR into a wallet. Both routes reach the chain eventually, but they hop through someone else's infrastructure. In this lesson you submit the signed transaction directly to a Cardano node using gOuroboros and the LocalTxSubmission mini-protocol.
This is the same path Dolos, a relay, or any node uses internally. No API keys, no hosted services.
When You'd Choose This Path
Submission route
Depends on
Good for
bfc.SubmitTx (Apollo + Blockfrost)
Blockfrost API + your key
Quick prototypes, testnets
Manual paste into wallet
A user in front of a wallet UI
End-user dApps
LocalTxSubmission via gOuroboros
A local node (Dolos)
Backend services, high volume, no third-party dependency
Once you run your own node, LocalTxSubmission is almost always the right choice for server-side Go code.
Prerequisites
- Completed 102.3 (you can produce an unsigned CBOR transaction) or 102.2 (signed CBOR)
- Completed 101.1 (starter-kit set up,
CARDANO_NODE_SOCKET_PATHandCARDANO_NODE_MAGICexported) - Dolos running on preprod
The Program You'll Use
gouroboros-starter-kit/cmd/tx-submission/main.go is a tiny wrapper around LocalTxSubmission.SubmitTx. It reads a signed transaction from disk (two formats supported) and hands the bytes to the node.
Two input formats, one required:
go run ./cmd/tx-submission -tx-file transaction.json # cardano-cli JSON format
# or
go run ./cmd/tx-submission -raw-tx-file transaction.cbor # raw CBOR bytes
The JSON format is compatible with what cardano-cli transaction sign produces:
{
"type": "Witnessed Tx ConwayEra",
"description": "Ledger Cddl Format",
"cborHex": "84a40081825820..."
}
The raw CBOR format is what you'd get if you Apollo-built, signed, and wrote the bytes directly.
Step 1: Produce a Signed Transaction
Use your 102.2 code path unchanged, but instead of calling bfc.SubmitTx(*tx), write the signed CBOR to a file:
// After apollob = apollob.Sign()
tx := apollob.GetTx()
cborBytes, err := cbor.Marshal(tx)
if err != nil {
panic(err)
}
if err := os.WriteFile("tx.cbor", cborBytes, 0600); err != nil {
panic(err)
}
fmt.Println("Wrote signed tx to tx.cbor")
Run your program. You now have a tx.cbor file in the current directory.
Don't skip signing. LocalTxSubmission expects a signed transaction. If you feed it the unsigned CBOR from 102.3, the node will reject it during phase-1 validation (missing witnesses).
Step 2: Copy the CBOR into the Starter-Kit
The tx-submission program needs to read the file, so place it somewhere go run can find it. Easiest:
cp tx.cbor /path/to/gouroboros-starter-kit/tx.cbor
cd /path/to/gouroboros-starter-kit
Step 3: Submit
Make sure your environment points at Dolos (from 101.1):
echo $CARDANO_NODE_SOCKET_PATH # should be your dolos.socket
echo $CARDANO_NODE_MAGIC # should be 1
Then:
go run ./cmd/tx-submission -raw-tx-file tx.cbor
Expected output (on success):
The transaction was accepted
That's it. The node has accepted the transaction into its mempool. Next time the chain advances (~20 seconds on preprod), a block producer will include it — exactly like Blockfrost would have routed it, just with one less hop.
Confirm It Landed
Two ways:
# Option 1: watch the mempool locally (from 101.4)
go run ./cmd/tx-monitor
# Option 2: check the explorer
# https://preprod.cardanoscan.io/transaction/<tx-hash>
You need the transaction hash to search on the explorer. If your program from Step 1 printed the hash (Apollo's tx.TransactionBody.Hash()), use that.
What the Program Is Actually Doing
Strip away the file parsing and flag handling, and tx-submission reduces to:
o, _ := ouroboros.NewConnection(
ouroboros.WithNetworkMagic(cfg.Magic),
ouroboros.WithErrorChan(errorChan),
ouroboros.WithNodeToNode(false),
ouroboros.WithKeepAlive(true),
ouroboros.WithLocalTxSubmissionConfig(localtxsubmission.NewConfig()),
)
o.Dial("unix", cfg.SocketPath)
txType, _ := ledger.DetermineTransactionType(txBytes)
err := o.LocalTxSubmission().Client.SubmitTx(uint16(txType), txBytes)
if err != nil {
fmt.Printf("ERROR: %s\n", err)
}
Two things worth knowing:
DetermineTransactionType(txBytes)inspects the CBOR prefix to pick the right era (Babbage, Conway, …). You don't pass era explicitly; the bytes tell the library.SubmitTx** is synchronous.** It either returns nil (accepted) or an error (rejected). Unlike Blockfrost, there is no "queued" middle state — the node either took it or didn't.
Common Errors and What They Mean
Error
Meaning
Fix
ApplyTxError ... UtxoFailure ... BadInputs
You spent a UTxO that no longer exists (already spent, or never existed)
Refetch UTxOs and rebuild — your chain view was stale
ApplyTxError ... OutsideValidityInterval
Your invalidBefore / invalidHereafter excludes the current slot
Widen the validity window (102.5) or rebuild with current slot
ApplyTxError ... FeeTooSmall
Fee below the protocol minimum
Let Apollo's Complete() set the fee — don't override unless you know why
handshake failed
Magic mismatch
CARDANO_NODE_MAGIC doesn't match Dolos's upstream
connect: no such file or directory
Dolos not running, or wrong socket path
Start dolos daemon, re-check path
The node's error message is usually specific enough to point at the cause. "Accepted" means phase-1 validation passed — not that the transaction is guaranteed to reach a block, but very close on a quiet testnet.
You'll Know You're Successful When
tx-submissionprints "The transaction was accepted"- Your transaction appears in the local mempool (
tx-monitor) within seconds - Within ~40 seconds, the transaction shows up on Cardanoscan preprod
- No Blockfrost API key was involved in the submission path
Practice Tasks
- Submit the same
tx.cbortwice. The second should fail — inputs are now consumed. Read the error carefully. - Build a signed tx for mainnet (don't actually do this with real funds), write it, and try submitting via your preprod Dolos. Note how the error tells you the magic is wrong.
- Compare the time from
tx-submissioncompleting to the tx appearing on the explorer. Is it faster, slower, or the same as submitting via Blockfrost?
What's Next
- 102.5 — add a validity window so your transaction is only valid for a bounded time
- 102.6 — attach metadata to a transaction
- Module 201 — watch these submissions stream past your own indexer