Build and submit transactions
Build a payment, sign it, and send it as three separate steps, so a person or an offline machine can approve it before anything is signed.
Ask your agent
Build a payment from agent-1 to <ADDRESS> for 10 XLM without submitting it, then show me the unsigned XDR.
Steps
-
Build, on a machine with network access:
stellar tx new payment --source <SOURCE> --destination <ADDRESS> --amount <AMOUNT> \--network <NETWORK> --build-only > unsigned.xdrThis writes unsigned base64 XDR to
unsigned.xdrand exits. It contacts RPC to read the source account's sequence number, but nothing is signed and nothing is submitted. -
Sign it. This step needs no RPC connection, so it is the one stage you can run offline. Always pass
--networkor--network-passphraseon this stage:stellar tx sign --sign-with-key <SOURCE> --network <NETWORK> < unsigned.xdr > signed.xdrA signature commits to the network passphrase. An unqualified
tx signfalls back to the saved default network, which can silently be the wrong one and produces a valid-looking envelope that fails to submit. -
Submit the signed envelope, back on a machine with network access:
stellar tx send --network <NETWORK> < signed.xdr
When no one needs to review the envelope, the three stages compose into one pipeline:
stellar tx new payment --source <SOURCE> --destination <ADDRESS> --amount <AMOUNT> \
--network <NETWORK> --build-only \
| stellar tx sign --sign-with-key <SOURCE> --network <NETWORK> \
| stellar tx send --network <NETWORK>
What --build-only skips
Every tx new <OPERATION> accepts --build-only, and so do the contract commands that submit a transaction (deploy, upload, invoke, extend, restore, asset deploy). It stops before signing and submitting. Read-only commands such as contract id asset do not take it. Building still reads the source account's sequence number over RPC, so it needs network access and a funded source account. token transfer and token approve do not accept --build-only, so use tx new payment --build-only for a payment handoff. tx sign, tx send, tx hash, tx simulate, tx decode, and the other envelope commands read XDR from stdin, so build, sign, and send compose into a pipeline. tx new builds from flags and tx fetch takes --hash; neither reads stdin.
Parsing the result
tx new <OPERATION> has no --output flag and writes nothing to stdout on success. The transaction hash appears only in the ℹ️ Signing transaction: <HASH> line on stderr, and --quiet removes that line along with everything else. This is not like token transfer, which prints the bare hash as its last stdout line and does support --output json.
To get a parseable receipt, let tx send produce it rather than trying to scrape an earlier stage. tx send, the last stage in step 3 above, returns JSON on success only, with top-level keys status, ledger, application_order, fee_bump, tx_hash, created_at, envelope, result, result_meta, and events. On success status is SUCCESS. The hash field is tx_hash, not hash. It has no --output flag, so the shape is always the indented one.
On a submission failure it writes zero bytes to stdout and puts the error on stderr. Empty stdout with a non-zero exit is therefore a definite failure, not an ambiguous one, and the exception below is specifically about the case where stdout has content you could not parse.
Never merge stderr into stdout here. tx send writes ℹ️ Transaction hash is <HASH> to stderr and the JSON receipt to stdout; 2>&1 interleaves them and breaks the parse. Unparseable content on stdout is not evidence of failure. The transaction may have already succeeded. Confirm with stellar tx fetch result --hash <HASH> --network <NETWORK> before retrying. Capture stderr to a file rather than discarding it; the hash you need for that check is only there.
Inspection before signing
Base64 XDR is not human-readable. Showing someone AAAAAgAAAABfEbrRS/av… and calling it an approval step is not an approval step: they cannot see the destination, the amount, or whether anything has already signed it. Render the envelope first:
stellar tx decode --output json-formatted < unsigned.xdr
That prints source_account, fee, seq_num, cond (the timebounds, "none" if there are none), memo, the operations array, ext, and signatures: [] on an unsigned envelope. tx decode needs no RPC connection. Do not skip memo when you review: it is the field that routes a deposit at an exchange, and a payment with the wrong memo reaches the right account and the wrong person.
None of those fields is the network. A mainnet payment and a testnet payment decode to an identical field set, so the rendered envelope alone cannot tell a reviewer which network they are approving. Carry the network alongside the envelope out of band: name it in the approval request, and keep testnet and mainnet envelopes in separate, differently named files. The account addresses are the only in-band hint, and they are a weak one, because the same key pair is valid on every network.
stellar tx hash then computes the envelope's hash without signing or submitting, also from stdin and also offline. Use it to confirm the thing you approved is the thing you are about to sign:
stellar tx hash --network <NETWORK> < unsigned.xdr
The hash is unchanged by signing, so the same value should come back after tx sign and from the network on submit.
Two tx hash calls compared against each other confirm the envelope is the same envelope, and nothing more. Both are computed from the passphrase you passed, so they agree even when tx sign used a different one.
To catch a wrong-network signature before you submit, compare tx hash against tx sign's own output. tx sign prints ℹ️ Signing transaction: <HASH> to stderr, computed under the passphrase it actually used. Both commands run offline:
stellar tx hash --network-passphrase "<INTENDED PASSPHRASE>" < unsigned.xdr
stellar tx sign --sign-with-key <IDENTITY> --network-passphrase "<INTENDED PASSPHRASE>" \
< unsigned.xdr > signed.xdr
Equal hashes mean the signature was made for the network you intended. Unequal means it was not, and the envelope is dead before you spend a submit on it.
Two conditions. Pass --network-passphrase explicitly to both commands: omit it on both and they resolve the same saved default, agree with each other, and pass on a wrong-network signature. And --quiet suppresses the tx sign line entirely, so the check and --quiet cannot be used together.
Check the destination while you are still at the build stage. A native payment cannot create an account, so paying an address that has never been funded fails at submit with TxFailed / OpInner(Payment(NoDestination)) and exit code 1. In this flow that is the worst place to find out, because a human has already reviewed and approved the envelope. stellar token balance --id native --account <ADDRESS> --output json answers it for free, and tx new create-account is the operation for an account that does not exist yet.
Air-gapped signing
Only tx sign and tx hash are genuinely offline. --build-only still needs RPC to read the source account's sequence number, so building cannot happen on a machine with no network access. The real split is three stages across as many machines as you want: build on a networked machine, sign on an offline machine, submit from a networked machine. Move only the unsigned XDR to the offline machine, sign it there, then move only the signed XDR back. The signing key never touching a networked machine is the actual security property, not that the whole pipeline avoids the network.
Soroban paths need a simulation step
A Soroban invocation's resource footprint depends on what the contract does, so stellar tx simulate --source-account <SOURCE> runs the transaction against current ledger state and returns the resource fee and footprint it would need, without submitting. contract invoke runs this simulation for you automatically. Composing your own tx new pipeline for a Soroban call means running simulate yourself before send.
22 operations, one reference page
stellar tx new <OPERATION> covers 22 operations, from payment and create-account to DEX and sponsorship operations. Each shares this same build, sign, send composition and adds its own flags on top. Run stellar tx new --help for the full operation list.
Common pitfalls
A pipeline breaks silently if an earlier stage fails but still writes something to stdout. Check the exit code of each stage, or run the pipeline one stage at a time the first time you compose it.
tx send can fail with TxBadSeq. The sequence number is fixed at build time, step 1, not at submit time, step 3. Anything else signing for the same source account in between, a concurrent agent or another one of your own commands, makes the envelope stale before it reaches step 3. stellar tx update sequence-number next fetches the source account's current sequence, increments it, and rewrites the envelope in place. It is a read-only network call.
It keeps the signatures the envelope already carried. Patching an envelope you have already signed and then signing again leaves two signatures on it, and the submit fails TxBadAuthExtra. Patch while the envelope is still unsigned, then sign:
stellar tx update sequence-number next --network <NETWORK> < unsigned.xdr > fixed.xdr
stellar tx sign --sign-with-key <IDENTITY> --network <NETWORK> < fixed.xdr | stellar tx send --network <NETWORK>
If the only copy you have is already signed, rebuild from step 1 instead. Measured on testnet, three steps each: patch-then-sign on an unsigned envelope succeeds, rebuild-then-sign succeeds, and patch-then-re-sign on a signed envelope fails TxBadAuthExtra every time.
Elapsed time alone is not the cause: only another transaction from the same source account advances the sequence. A long delay is harmless unless the envelope carries timebounds, which tx decode shows as cond.
Two --build-only calls against the same source return the same sequence number. --build-only reads the account's current sequence and adds one; it reserves nothing and advances nothing. An agent that batch-builds several envelopes before submitting any of them gets a set that all collide, and only the first can land. Build and submit one at a time, or patch each envelope with the command above before you send it.