diff --git a/docs.json b/docs.json
index 071f323..445f444 100644
--- a/docs.json
+++ b/docs.json
@@ -125,6 +125,9 @@
"group": "Operations",
"pages": [
"guides/stellar-mainnet-deployment",
+ "guides/stellar-multisig-withdrawal",
+ "guides/stellar-offline-signing",
+ "guides/stellar-tx-simulation"
"guides/stellar-multisig-withdrawal",
"guides/privacy-best-practices",
"guides/spectre-stellar-cookbook",
diff --git a/guides/privacy-best-practices.mdx b/guides/privacy-best-practices.mdx
index 3fba0fa..ae66633 100644
--- a/guides/privacy-best-practices.mdx
+++ b/guides/privacy-best-practices.mdx
@@ -175,3 +175,4 @@ Identical amounts create a fingerprint. Consider varying the amount.
- [Stellar Multisig Stealth Withdrawals](/guides/stellar-multisig-withdrawal) — additional coordination and recovery considerations for multisig source accounts
- [Stellar Offline Transaction Signing](/guides/stellar-offline-signing) — keep stealth spending keys fully air-gapped using the offline signing workflow
+- [Stellar Transaction Simulation](/guides/stellar-tx-simulation) — decode contract errors before fees are spent or signatures collected
diff --git a/guides/stellar-tx-simulation.mdx b/guides/stellar-tx-simulation.mdx
new file mode 100644
index 0000000..2ea6edf
--- /dev/null
+++ b/guides/stellar-tx-simulation.mdx
@@ -0,0 +1,437 @@
+---
+title: "Stellar Transaction Simulation"
+description: "Use Soroban simulateTransaction for safe pre-flight checks before signing and submitting stealth payment transactions"
+---
+
+`simulateTransaction` is a Soroban RPC method that runs a transaction against the current ledger state without submitting it. It returns resource estimates, output previews, and — critically — decoded contract errors before any fees are spent or signatures collected. For stealth payment flows, simulation should be run on every contract call before signing.
+
+
+ Simulation applies to **Soroban contract invocations** — contract calls such as `announce`,
+ `register`, and `send`. Plain Stellar operations (native `Payment`, `CreateAccount`) do not need
+ simulation, though the Soroban SDK's `prepareTransaction` helper runs it automatically for
+ contract calls.
+
+
+---
+
+## What Simulation Does
+
+| Output | What it tells you |
+|---|---|
+| **Footprint** (read/write ledger keys) | Which contract storage entries the transaction touches |
+| **Resource estimates** | CPU instructions, memory bytes, ledger I/O — needed to set the `sorobanData` field |
+| **Return value** | The contract's return value, XDR-encoded — decode it to preview the result |
+| **Events** | Contract events that would be emitted — useful for verifying announcement data |
+| **Error** | If the contract would revert, the full error including contract-defined error codes |
+| **Fee estimate** | Minimum fee to include for the transaction to succeed |
+
+Without simulation, a contract call that would fail on-chain costs fees and consumes a sequence number. With simulation, you catch errors for free before signing.
+
+---
+
+## When to Use Simulation
+
+| Scenario | Simulate? |
+|---|---|
+| Invoking any Soroban contract (`announce`, `register`, `send`) | **Always** |
+| Plain `Payment` or `CreateAccount` operation | Not needed |
+| Before collecting multisig signatures | **Yes** — fail fast before distributing XDR to signers |
+| Before offline signing | **Yes** — simulate online, sign offline with confidence |
+| After any contract upgrade or parameter change | **Yes** — footprints and fees may have changed |
+
+Simulation is especially important in stealth flows because the `stealth-announcer` and `stealth-sender` contracts involve persistent storage writes. A misconfigured call (wrong asset, insufficient balance, unregistered key) returns a contract error that simulation exposes immediately.
+
+---
+
+## SDK Helpers
+
+The Soroban SDK's `SorobanRpc.Server` exposes `prepareTransaction`, which runs simulation internally and returns a transaction ready to sign. Use it as the standard path for all contract calls.
+
+### `prepareTransaction` — the standard path
+
+```typescript
+import {
+ Contract,
+ TransactionBuilder,
+ BASE_FEE,
+ nativeToScVal,
+ Address,
+} from "@stellar/stellar-sdk";
+import { SorobanRpc } from "@stellar/stellar-sdk";
+
+const FUTURENET_RPC = "https://rpc-futurenet.stellar.org";
+const FUTURENET_PASSPHRASE = "Test SDF Future Network ; October 2022";
+
+const server = new SorobanRpc.Server(FUTURENET_RPC);
+
+async function buildAndPrepareContractCall(params: {
+ callerAccountId: string;
+ contractId: string;
+ method: string;
+ args: ReturnType[];
+}) {
+ const { callerAccountId, contractId, method, args } = params;
+
+ const account = await server.getAccount(callerAccountId);
+ const contract = new Contract(contractId);
+
+ // Build the raw transaction (no resource data yet)
+ const rawTx = new TransactionBuilder(account, {
+ fee: BASE_FEE,
+ networkPassphrase: FUTURENET_PASSPHRASE,
+ })
+ .addOperation(contract.call(method, ...args))
+ .setTimeout(300)
+ .build();
+
+ // prepareTransaction runs simulateTransaction internally,
+ // attaches the footprint and resource estimates, and returns
+ // a transaction ready to sign.
+ const preparedTx = await server.prepareTransaction(rawTx);
+
+ return preparedTx;
+}
+```
+
+`prepareTransaction` throws a `SorobanRpc.SimulateTransactionError` if the simulation fails, with the raw simulation result attached. Catch it to decode the contract error:
+
+```typescript
+import { SorobanRpc, xdr } from "@stellar/stellar-sdk";
+
+try {
+ const preparedTx = await buildAndPrepareContractCall({
+ callerAccountId: "G...CALLER",
+ contractId: "C...ANNOUNCER",
+ method: "announce",
+ args: [
+ // stealth_address, ephemeral_pub_key, metadata
+ ],
+ });
+
+ // Sign and submit the prepared transaction
+ preparedTx.sign(callerKeypair);
+ const result = await server.sendTransaction(preparedTx);
+ console.log("Submitted:", result.hash);
+
+} catch (e) {
+ if (e instanceof SorobanRpc.SimulateTransactionError) {
+ console.error("Simulation failed:", e.message);
+ console.error("Raw simulation result:", JSON.stringify(e.result, null, 2));
+ // Decode the contract error — see "Handling Sim Failures" below
+ } else {
+ throw e;
+ }
+}
+```
+
+---
+
+## Raw RPC Patterns
+
+When you need full control over the simulation result — for logging, fee inspection, or event preview — call `simulateTransaction` directly before preparing.
+
+### Direct simulation
+
+```typescript
+import { SorobanRpc, Transaction } from "@stellar/stellar-sdk";
+
+async function simulate(rawTxXdr: string): Promise {
+ const tx = new Transaction(rawTxXdr, FUTURENET_PASSPHRASE);
+ const result = await server.simulateTransaction(tx);
+ return result;
+}
+
+const simResult = await simulate(rawTx.toXDR());
+
+if (SorobanRpc.Api.isSimulationError(simResult)) {
+ console.error("Simulation error:", simResult.error);
+} else if (SorobanRpc.Api.isSimulationRestore(simResult)) {
+ console.warn("State restore required before this transaction can succeed");
+} else {
+ console.log("Simulation success");
+ console.log("Min resource fee:", simResult.minResourceFee, "stroops");
+}
+```
+
+### Applying simulation results manually
+
+If you need to apply simulation results to a transaction yourself (advanced use cases):
+
+```typescript
+import { SorobanRpc, assembleTransaction } from "@stellar/stellar-sdk";
+
+// assembleTransaction applies the footprint + resource estimates from the sim result
+// This is what prepareTransaction does internally
+const assembledTx = assembleTransaction(rawTx, simResult);
+```
+
+---
+
+## Decoding Simulation Results
+
+### Return values
+
+The contract's return value is an XDR `ScVal`. Decode it with `scValToNative`:
+
+```typescript
+import { scValToNative, SorobanRpc } from "@stellar/stellar-sdk";
+
+if (!SorobanRpc.Api.isSimulationError(simResult) && simResult.result) {
+ const returnValue = scValToNative(simResult.result.retval);
+ console.log("Contract return value:", returnValue);
+}
+```
+
+### Events
+
+Simulated events appear in `simResult.events`. Each is a base64-encoded XDR `DiagnosticEvent`:
+
+```typescript
+import { xdr, scValToNative } from "@stellar/stellar-sdk";
+
+if (!SorobanRpc.Api.isSimulationError(simResult)) {
+ for (const rawEvent of simResult.events ?? []) {
+ const event = xdr.DiagnosticEvent.fromXDR(rawEvent.event, "base64");
+
+ if (event.event().type().name !== "contract") continue;
+
+ const contractEvent = event.event().body().v0();
+ const topics = contractEvent.topics().map(t => scValToNative(t));
+ const data = scValToNative(contractEvent.data());
+
+ console.log("Event topics:", topics);
+ console.log("Event data: ", data);
+ }
+}
+```
+
+For the `stealth-announcer` contract, you'll see a topic array like:
+```
+["announce", schemeId, stealthAddress, caller]
+```
+and data containing the ephemeral public key and view tag metadata — exactly what the scanner expects.
+
+### Fee breakdown
+
+```typescript
+if (!SorobanRpc.Api.isSimulationError(simResult)) {
+ console.log("Min resource fee:", simResult.minResourceFee, "stroops");
+
+ const resources = simResult.transactionData?.build().resources();
+ if (resources) {
+ console.log("CPU instructions:", resources.instructions());
+ console.log("Read bytes: ", resources.readBytes());
+ console.log("Write bytes: ", resources.writeBytes());
+ }
+}
+```
+
+Use `minResourceFee` to set a realistic fee on the transaction. The `BASE_FEE` (100 stroops) only covers the base transaction fee; Soroban resource fees are additional.
+
+---
+
+## Handling Simulation Failures
+
+Contract errors returned by simulation are far more actionable than opaque on-chain failure codes. Decode them to give users useful feedback.
+
+### Decoding a contract error
+
+```typescript
+import { xdr, SorobanRpc, scValToNative } from "@stellar/stellar-sdk";
+
+function decodeSimulationError(simResult: SorobanRpc.Api.SimulateTransactionResponse): string {
+ if (!SorobanRpc.Api.isSimulationError(simResult)) return "No error";
+
+ const errorStr = simResult.error;
+
+ // Contract errors often contain an XDR-encoded ScError
+ try {
+ const scError = xdr.ScVal.fromXDR(errorStr, "base64");
+ const decoded = scValToNative(scError);
+ return `Contract error: ${JSON.stringify(decoded)}`;
+ } catch {
+ // Not XDR — return the raw error string
+ return errorStr;
+ }
+}
+```
+
+### Common simulation errors and fixes
+
+| Error | Cause | Fix |
+|---|---|---|
+| `HostError: Error(Contract, #1)` | Contract-defined error code 1 — check the contract's error enum | Read the contract's error documentation for code meanings |
+| `HostError: Error(WasmVm, InvalidAction)` | Contract called with wrong argument types | Check `ScVal` types match the contract function signature |
+| `HostError: Error(Storage, MissingValue)` | A required ledger entry doesn't exist | Ensure the account or contract data is initialized first |
+| `isSimulationRestore` | An archived ledger entry must be restored before proceeding | Submit a `RestoreFootprint` transaction first |
+| Insufficient balance | Native balance too low for the operation | Fund the account via Friendbot (testnet) or direct transfer |
+
+### User-facing error messages for stealth flows
+
+```typescript
+import { SorobanRpc } from "@stellar/stellar-sdk";
+
+function userFacingSimError(simResult: SorobanRpc.Api.SimulateTransactionResponse): string {
+ if (!SorobanRpc.Api.isSimulationError(simResult)) return "";
+
+ const raw = simResult.error.toLowerCase();
+
+ if (raw.includes("missingvalue")) {
+ return "The stealth address or registry entry was not found. Ensure the recipient is registered.";
+ }
+ if (raw.includes("insufficient") || raw.includes("underfunded")) {
+ return "Insufficient balance. Top up the account before sending.";
+ }
+ if (SorobanRpc.Api.isSimulationRestore(simResult)) {
+ return "A state restore is required. Please retry — the restore will be handled automatically.";
+ }
+
+ return `Transaction pre-flight failed: ${simResult.error}`;
+}
+```
+
+---
+
+## End-to-End: Simulating a Stealth Announcement
+
+This shows the full simulation flow for a `stealth-announcer` contract call — the most common Soroban interaction in a stealth payment flow.
+
+```typescript
+import {
+ Contract,
+ TransactionBuilder,
+ BASE_FEE,
+ Keypair,
+ nativeToScVal,
+ xdr,
+ scValToNative,
+ Address,
+} from "@stellar/stellar-sdk";
+import { SorobanRpc } from "@stellar/stellar-sdk";
+import {
+ generateStealthAddress,
+ deriveStealthKeys,
+ getDeployment,
+ STEALTH_SIGNING_MESSAGE,
+} from "@wraith-protocol/sdk/chains/stellar";
+
+const FUTURENET_RPC = "https://rpc-futurenet.stellar.org";
+const FUTURENET_PASSPHRASE = "Test SDF Future Network ; October 2022";
+const server = new SorobanRpc.Server(FUTURENET_RPC);
+
+async function simulateAndSendAnnouncement(params: {
+ callerKeypair: Keypair;
+ recipientSpendingPubKey: Uint8Array;
+ recipientViewingPubKey: Uint8Array;
+}): Promise {
+ const { callerKeypair, recipientSpendingPubKey, recipientViewingPubKey } = params;
+
+ // 1. Generate a stealth address for the recipient
+ const { stealthAddress, ephemeralPubKey, viewTag } = generateStealthAddress(
+ recipientSpendingPubKey,
+ recipientViewingPubKey
+ );
+
+ // 2. Build the raw contract call
+ const deployment = getDeployment("futurenet");
+ const contract = new Contract(deployment.contracts.announcer);
+ const account = await server.getAccount(callerKeypair.publicKey());
+
+ const metadata = Buffer.alloc(1);
+ metadata[0] = viewTag;
+
+ const rawTx = new TransactionBuilder(account, {
+ fee: BASE_FEE,
+ networkPassphrase: FUTURENET_PASSPHRASE,
+ })
+ .addOperation(
+ contract.call(
+ "announce",
+ nativeToScVal(1, { type: "u32" }), // scheme_id
+ new Address(stealthAddress).toScVal(), // stealth_address
+ nativeToScVal(ephemeralPubKey, { type: "bytes" }), // ephemeral_pub_key
+ nativeToScVal(metadata, { type: "bytes" }), // metadata (view tag)
+ )
+ )
+ .setTimeout(300)
+ .build();
+
+ // 3. Simulate first — inspect fees and events before signing
+ console.log("Running pre-flight simulation...");
+ const simResult = await server.simulateTransaction(rawTx);
+
+ if (SorobanRpc.Api.isSimulationError(simResult)) {
+ const msg = userFacingSimError(simResult);
+ throw new Error(`Pre-flight failed: ${msg}`);
+ }
+
+ // 4. Log the fee estimate and preview events
+ console.log("Min resource fee:", simResult.minResourceFee, "stroops");
+ console.log("Events that will be emitted:");
+
+ for (const rawEvent of simResult.events ?? []) {
+ const event = xdr.DiagnosticEvent.fromXDR(rawEvent.event, "base64");
+ if (event.event().type().name !== "contract") continue;
+ const topics = event.event().body().v0().topics().map(t => scValToNative(t));
+ console.log(" -", topics);
+ }
+
+ // 5. Assemble (apply footprint + resources), sign, and submit
+ const preparedTx = await server.prepareTransaction(rawTx);
+ preparedTx.sign(callerKeypair);
+
+ const submitResult = await server.sendTransaction(preparedTx);
+ if (submitResult.status === "ERROR") {
+ throw new Error(`Submission failed: ${JSON.stringify(submitResult.errorResult)}`);
+ }
+
+ console.log("Submitted announcement:", submitResult.hash);
+
+ // 6. Poll for confirmation
+ let status = await server.getTransaction(submitResult.hash);
+ while (status.status === "NOT_FOUND") {
+ await new Promise(r => setTimeout(r, 3000));
+ status = await server.getTransaction(submitResult.hash);
+ }
+
+ if (status.status === "SUCCESS") {
+ console.log("Announcement confirmed in ledger", status.ledger);
+ console.log("Stealth address:", stealthAddress);
+ } else {
+ console.error("Transaction failed. Result XDR:", status.resultXdr);
+ }
+}
+
+// Run on futurenet
+const callerKeypair = Keypair.random(); // funded via Friendbot
+const recipientMaster = Keypair.random();
+const recipientSig = recipientMaster.sign(Buffer.from(STEALTH_SIGNING_MESSAGE));
+const { spendingPubKey, viewingPubKey } = deriveStealthKeys(recipientSig);
+
+await simulateAndSendAnnouncement({
+ callerKeypair,
+ recipientSpendingPubKey: spendingPubKey,
+ recipientViewingPubKey: viewingPubKey,
+});
+```
+
+### Expected output
+
+```
+Running pre-flight simulation...
+Min resource fee: 47312 stroops
+Events that will be emitted:
+ - [ 'announce', 1, 'GXYZ...', 'GABC...' ]
+Submitted announcement: a1b2c3...
+Announcement confirmed in ledger 8294710
+Stealth address: GXYZ...
+```
+
+---
+
+## See Also
+
+- [Stellar Primitives](/sdk/chains/stellar) — full SDK reference including `generateStealthAddress` and `scanAnnouncements`
+- [Stellar Offline Transaction Signing](/guides/stellar-offline-signing) — simulate online, sign on an air-gapped machine
+- [Stellar Multisig Stealth Withdrawals](/guides/stellar-multisig-withdrawal) — simulate before distributing XDR to N signers
+- [Stellar Event Schemas](/reference/stellar-event-schemas) — announcer event topic format reference
\ No newline at end of file
diff --git a/sdk/chains/stellar.mdx b/sdk/chains/stellar.mdx
index cf88d10..fe2f59d 100644
--- a/sdk/chains/stellar.mdx
+++ b/sdk/chains/stellar.mdx
@@ -423,6 +423,8 @@ This replaces the need to manually query `sorobanServer.getEvents()` and parse X
> [!NOTE]
> For advanced use cases and indexer building, refer to the [Stellar Event Schemas (v2)](/reference/stellar-event-schemas) documentation to learn how to natively filter topics via the RPC.
+- [Stellar Offline Transaction Signing](/guides/stellar-offline-signing) — build online, sign air-gapped, submit without the private key touching the internet
+- [Stellar Transaction Simulation](/guides/stellar-tx-simulation) — pre-flight every Soroban contract call with simulateTransaction before signing
---