diff --git a/docs.json b/docs.json
index ab3d6de..dc10f10 100644
--- a/docs.json
+++ b/docs.json
@@ -124,7 +124,10 @@
"groups": [
{
"group": "Quickstarts",
- "pages": ["guides/quickstarts/rust"]
+ "pages": [
+ "guides/quickstarts/python",
+ "guides/quickstarts/rust"
+ ]
},
{
"group": "Guides",
diff --git a/guides/quickstarts/python.mdx b/guides/quickstarts/python.mdx
new file mode 100644
index 0000000..9c5ad87
--- /dev/null
+++ b/guides/quickstarts/python.mdx
@@ -0,0 +1,893 @@
+---
+title: "Python Quickstart"
+description: "Derive stealth keys, fetch announcements, scan for owned payments, and send a stealth transfer — all from Python 3.11+ using stellar-sdk and a small crypto utility."
+keywords: "Python, stellar-sdk, stealth payment, Soroban RPC, ed25519, X25519, futurenet, scan key"
+---
+
+
+ This guide targets the Wraith contracts on the Stellar **futurenet**. The
+ derivation crypto and RPC calls are identical on testnet and mainnet — only the
+ `soroban_rpc_url`, `network_passphrase`, and `announcer_contract_id` differ.
+ See [Stellar Networks](/reference/stellar-networks) for the full address table.
+
+
+By the end of this guide you will have:
+
+- Derived stealth spending and viewing keys from a Stellar keypair in pure Python
+- Fetched announcement events from the Soroban RPC without the TypeScript SDK
+- Scanned those announcements to find payments addressed to you
+- Sent a stealth payment and published an on-chain announcement
+
+**Prerequisites:** Python 3.11+, `pip`, and a Stellar keypair you can fund on futurenet.
+
+---
+
+## Step 1 — Install dependencies
+
+```bash no-check
+pip install stellar-sdk==11.* cryptography
+```
+
+`stellar-sdk` handles Soroban RPC calls, XDR encoding, transaction building, and
+`StrKey` address encoding. `cryptography` provides X25519 Diffie-Hellman needed for
+the shared-secret step.
+
+No TypeScript toolchain is required.
+
+---
+
+## Step 2 — Create `wraith_crypto.py`
+
+This utility file implements the three primitives the rest of the guide relies on:
+**key derivation**, **stealth address generation**, and **announcement scanning**. It
+is intentionally self-contained — copy it into your project and import from it.
+
+```python no-check
+# wraith_crypto.py
+"""
+Wraith Protocol — Stellar stealth-address crypto, pure Python.
+
+Implements the same derivation as @wraith-protocol/sdk/chains/stellar:
+ - SHA-256 domain separation for spending/viewing seeds
+ - X25519 ECDH via Edwards-to-Montgomery conversion
+ - View-tag prefilter (1 byte, ~255/256 skip rate)
+ - ed25519 scalar addition for stealth pub-key derivation
+
+Requires: cryptography>=41, stellar-sdk>=11
+"""
+
+from __future__ import annotations
+
+import hashlib
+import hmac
+import struct
+from dataclasses import dataclass
+from typing import Optional
+
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
+from cryptography.hazmat.primitives.serialization import (
+ Encoding,
+ PublicFormat,
+ PrivateFormat,
+ NoEncryption,
+)
+from stellar_sdk import Keypair
+
+# ---------------------------------------------------------------------------
+# Constants (mirrors constants.ts in the SDK)
+# ---------------------------------------------------------------------------
+
+STEALTH_SIGNING_MESSAGE = (
+ "Sign this message to generate your Wraith stealth keys.\n\n"
+ "Chain: Stellar\n"
+ "Note: This signature is used for key derivation only and does not "
+ "authorize any transaction."
+)
+
+META_ADDRESS_PREFIX = "st:xlm:"
+SCHEME_ID = 1
+
+# ed25519 group order L
+L = 2**252 + 27742317777372353535851937790883648493
+
+
+# ---------------------------------------------------------------------------
+# Low-level helpers
+# ---------------------------------------------------------------------------
+
+def _sha256(*parts: bytes) -> bytes:
+ h = hashlib.sha256()
+ for part in parts:
+ h.update(part)
+ return h.digest()
+
+
+def _sha512(data: bytes) -> bytes:
+ return hashlib.sha512(data).digest()
+
+
+def _clamp_scalar(scalar_bytes: bytearray) -> bytearray:
+ """Apply standard ed25519 scalar clamping."""
+ scalar_bytes[0] &= 248
+ scalar_bytes[31] &= 127
+ scalar_bytes[31] |= 64
+ return scalar_bytes
+
+
+def _seed_to_scalar(seed: bytes) -> int:
+ """
+ Expand a 32-byte seed to its clamped scalar (mirrors seedToScalar in the SDK).
+
+ 1. h = SHA-512(seed)
+ 2. a = h[0:32] (lower half)
+ 3. Clamp a
+ 4. Interpret as little-endian bigint
+ """
+ h = _sha512(seed)
+ a = bytearray(h[:32])
+ _clamp_scalar(a)
+ return int.from_bytes(a, "little")
+
+
+def _ed25519_pub_from_seed(seed: bytes) -> bytes:
+ """Return the 32-byte compressed ed25519 public key for a seed."""
+ priv = Ed25519PrivateKey.from_private_bytes(seed)
+ return priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
+
+
+# ---------------------------------------------------------------------------
+# Edwards-to-Montgomery conversion (RFC 7748)
+# ---------------------------------------------------------------------------
+
+# Ed25519 curve parameters
+_P = 2**255 - 19
+_D = -121665 * pow(121666, _P - 2, _P) % _P
+
+
+def _decompress_ed25519(point_bytes: bytes) -> tuple[int, int]:
+ """Decompress a 32-byte compressed ed25519 point to (x, y)."""
+ y_int = int.from_bytes(point_bytes, "little")
+ sign = (y_int >> 255) & 1
+ y = y_int & ((1 << 255) - 1)
+
+ # Recover x from the curve equation: -x^2 + y^2 = 1 + d*x^2*y^2
+ y2 = (y * y) % _P
+ x2 = (y2 - 1) * pow(1 + _D * y2, _P - 2, _P) % _P
+ x = pow(x2, (_P + 3) // 8, _P)
+
+ if (x * x - x2) % _P != 0:
+ _I = pow(2, (_P - 1) // 4, _P)
+ x = x * _I % _P
+
+ if x % 2 != sign:
+ x = _P - x
+
+ return x, y
+
+
+def _edwards_to_montgomery_pub(ed_pub: bytes) -> bytes:
+ """
+ Convert an ed25519 public key (Edwards form) to an X25519 key (Montgomery form).
+ u = (1 + y) / (1 - y) mod p
+ """
+ _, y = _decompress_ed25519(ed_pub)
+ u = (1 + y) * pow(1 - y, _P - 2, _P) % _P
+ return u.to_bytes(32, "little")
+
+
+def _edwards_to_montgomery_priv(ed_seed: bytes) -> bytes:
+ """
+ Convert an ed25519 seed to the X25519 scalar (lower 32 bytes of SHA-512, clamped).
+ """
+ h = _sha512(ed_seed)
+ a = bytearray(h[:32])
+ _clamp_scalar(a)
+ return bytes(a)
+
+
+# ---------------------------------------------------------------------------
+# Key structures
+# ---------------------------------------------------------------------------
+
+@dataclass
+class StealthKeys:
+ spending_key: bytes # 32-byte seed
+ spending_scalar: int # clamped bigint
+ viewing_key: bytes # 32-byte seed
+ viewing_scalar: int # clamped bigint
+ spending_pub_key: bytes # 32-byte ed25519 public key
+ viewing_pub_key: bytes # 32-byte ed25519 public key
+
+
+@dataclass
+class GeneratedStealthAddress:
+ stealth_address: str # Stellar G... address
+ ephemeral_pub_key: bytes # 32-byte ed25519 public key
+ view_tag: int # 0–255
+
+
+@dataclass
+class MatchedAnnouncement:
+ stealth_address: str
+ ephemeral_pub_key: str # hex
+ stealth_private_scalar: int
+ stealth_pub_key_bytes: bytes
+
+
+# ---------------------------------------------------------------------------
+# Core API
+# ---------------------------------------------------------------------------
+
+def derive_stealth_keys(signature: bytes) -> StealthKeys:
+ """
+ Derive stealth spending and viewing key pairs from a 64-byte ed25519 signature.
+
+ Algorithm (mirrors deriveStealthKeys in SDK keys.ts):
+ spending_key = SHA-256("wraith:spending:" || signature)
+ viewing_key = SHA-256("wraith:viewing:" || signature)
+
+ Domain-separated hashing is used (instead of splitting r/s) because
+ ed25519 signature components lack the independence of secp256k1 components.
+ """
+ spending_key = _sha256(b"wraith:spending:", signature)
+ viewing_key = _sha256(b"wraith:viewing:", signature)
+
+ return StealthKeys(
+ spending_key=spending_key,
+ spending_scalar=_seed_to_scalar(spending_key),
+ viewing_key=viewing_key,
+ viewing_scalar=_seed_to_scalar(viewing_key),
+ spending_pub_key=_ed25519_pub_from_seed(spending_key),
+ viewing_pub_key=_ed25519_pub_from_seed(viewing_key),
+ )
+
+
+def _x25519_shared_secret(priv_ed_seed: bytes, pub_ed: bytes) -> bytes:
+ """
+ Compute X25519 shared secret from an ed25519 seed and an ed25519 public key.
+ Converts both keys to Montgomery form before the DH operation (RFC 7748).
+ """
+ priv_mont = _edwards_to_montgomery_priv(priv_ed_seed)
+ pub_mont = _edwards_to_montgomery_pub(pub_ed)
+
+ x_priv = X25519PrivateKey.from_private_bytes(priv_mont)
+ x_pub = X25519PublicKey.from_public_bytes(pub_mont)
+ return x_priv.exchange(x_pub)
+
+
+def _compute_view_tag(shared_secret: bytes) -> int:
+ """
+ Compute the 1-byte view tag (v2 domain).
+ view_tag = SHA-256("wraith:stellar:view-tag:v2:" || shared_secret)[0]
+ """
+ digest = _sha256(b"wraith:stellar:view-tag:v2:", shared_secret)
+ return digest[0]
+
+
+def _hash_to_scalar(shared_secret: bytes) -> int:
+ """
+ Hash the shared secret to a scalar reduced mod L.
+ SHA-256("wraith:scalar:" || shared_secret) interpreted as little-endian bigint.
+ """
+ digest = _sha256(b"wraith:scalar:", shared_secret)
+ return int.from_bytes(digest, "little") % L
+
+
+def _ed25519_point_add(pub_bytes: bytes, scalar: int) -> bytes:
+ """
+ Compute P + scalar*G on ed25519 and return the compressed 32-byte result.
+ Uses the cryptography library's internal scalar multiplication via a trick:
+ we derive scalar*G from a raw scalar key, then add to P via field arithmetic.
+
+ This mirrors deriveStealthPubKey in the SDK (scalar.ts point addition).
+ """
+ # scalar * G → get the public key for scalar as a raw scalar
+ # We represent scalar as a little-endian 32-byte value and manually construct
+ # the point using the standard ed25519 signing key structure.
+ # Note: we're doing point addition in the ed25519 group.
+
+ # Decompress both points
+ x1, y1 = _decompress_ed25519(pub_bytes)
+
+ # scalar * G: derive public key bytes from scalar
+ scalar_bytes = scalar.to_bytes(32, "little")
+ scalar_ed = Ed25519PrivateKey.from_private_bytes(scalar_bytes)
+ scalar_g_bytes = scalar_ed.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
+ x2, y2 = _decompress_ed25519(scalar_g_bytes)
+
+ # Edwards point addition formula:
+ # x3 = (x1*y2 + y1*x2) / (1 + d*x1*x2*y1*y2)
+ # y3 = (y1*y2 + x1*x2) / (1 - d*x1*x2*y1*y2)
+ x1y2 = x1 * y2 % _P
+ y1x2 = y1 * x2 % _P
+ x1x2 = x1 * x2 % _P
+ y1y2 = y1 * y2 % _P
+ dx1x2y1y2 = _D * x1x2 * y1y2 % _P
+
+ x3 = (x1y2 + y1x2) * pow(1 + dx1x2y1y2, _P - 2, _P) % _P
+ y3 = (y1y2 + x1x2) * pow(1 - dx1x2y1y2, _P - 2, _P) % _P
+
+ # Compress: encode y with sign bit of x
+ result = bytearray(y3.to_bytes(32, "little"))
+ if x3 & 1:
+ result[31] |= 0x80
+ return bytes(result)
+
+
+def _pub_key_to_stellar_address(pub_key: bytes) -> str:
+ """Convert a 32-byte ed25519 public key to a Stellar G... address (StrKey)."""
+ return Keypair.from_raw_ed25519_public_key(pub_key).public_key
+
+
+def generate_stealth_address(
+ spending_pub_key: bytes,
+ viewing_pub_key: bytes,
+ ephemeral_seed: Optional[bytes] = None,
+) -> GeneratedStealthAddress:
+ """
+ Generate a one-time stealth address for a Stellar recipient.
+
+ If ephemeral_seed is None, a fresh random seed is generated.
+ Providing a fixed seed is useful for deterministic tests only.
+
+ Algorithm (mirrors generateStealthAddress in SDK stealth.ts):
+ 1. Ephemeral ed25519 key pair
+ 2. X25519 ECDH shared secret with recipient's viewing key
+ 3. view_tag = SHA-256("wraith:stellar:view-tag:v2:" || shared)[0]
+ 4. h_scalar = SHA-256("wraith:scalar:" || shared) mod L
+ 5. stealth_point = spending_pub + h_scalar * G (ed25519 point addition)
+ 6. Encode as Stellar G... via StrKey
+ """
+ import os
+ if ephemeral_seed is None:
+ ephemeral_seed = os.urandom(32)
+
+ ephemeral_pub = _ed25519_pub_from_seed(ephemeral_seed)
+ shared = _x25519_shared_secret(ephemeral_seed, viewing_pub_key)
+
+ view_tag = _compute_view_tag(shared)
+ h_scalar = _hash_to_scalar(shared)
+
+ stealth_pub = _ed25519_point_add(spending_pub_key, h_scalar)
+ stealth_address = _pub_key_to_stellar_address(stealth_pub)
+
+ return GeneratedStealthAddress(
+ stealth_address=stealth_address,
+ ephemeral_pub_key=ephemeral_pub,
+ view_tag=view_tag,
+ )
+
+
+def encode_meta_address(spending_pub_key: bytes, viewing_pub_key: bytes) -> str:
+ """Encode two 32-byte public keys into a Stellar stealth meta-address."""
+ return META_ADDRESS_PREFIX + spending_pub_key.hex() + viewing_pub_key.hex()
+
+
+def decode_meta_address(meta_address: str) -> tuple[bytes, bytes]:
+ """Decode a Stellar meta-address into (spending_pub_key, viewing_pub_key)."""
+ if not meta_address.startswith(META_ADDRESS_PREFIX):
+ raise ValueError(f"Expected prefix '{META_ADDRESS_PREFIX}'")
+ payload = meta_address[len(META_ADDRESS_PREFIX):]
+ if len(payload) != 128:
+ raise ValueError("Meta-address payload must be 128 hex chars (2 × 32 bytes)")
+ spending = bytes.fromhex(payload[:64])
+ viewing = bytes.fromhex(payload[64:])
+ return spending, viewing
+
+
+def scan_announcements(
+ announcements: list[dict],
+ viewing_key: bytes,
+ spending_pub_key: bytes,
+ spending_scalar: int,
+) -> list[MatchedAnnouncement]:
+ """
+ Scan a list of announcement dicts for payments addressed to these keys.
+
+ Each announcement dict must have:
+ ephemeral_pub_key : hex string (64 chars, 32 bytes)
+ metadata : hex string (first byte = view tag)
+ stealth_address : str (G... address)
+
+ Returns only announcements that match — with the derived private scalar
+ needed to spend the funds.
+
+ View-tag prefilter: only ~1/256 non-matching announcements reach the
+ expensive X25519 step (false-positive rate ≈ 0.39 %).
+ """
+ results: list[MatchedAnnouncement] = []
+
+ for ann in announcements:
+ eph_bytes = bytes.fromhex(ann["ephemeral_pub_key"])
+ meta_bytes = bytes.fromhex(ann["metadata"])
+ announced_view_tag = meta_bytes[0]
+
+ # ------------------------------------------------------------------
+ # Cheap prefilter: compute expected view tag and compare
+ # ------------------------------------------------------------------
+ shared = _x25519_shared_secret(viewing_key, eph_bytes)
+ expected_tag = _compute_view_tag(shared)
+ if expected_tag != announced_view_tag:
+ continue # skip ~255/256 of non-matching entries
+
+ # ------------------------------------------------------------------
+ # Full check: derive the expected stealth address and compare
+ # ------------------------------------------------------------------
+ h_scalar = _hash_to_scalar(shared)
+ stealth_pub = _ed25519_point_add(spending_pub_key, h_scalar)
+ derived_address = _pub_key_to_stellar_address(stealth_pub)
+
+ if derived_address != ann["stealth_address"]:
+ continue # view-tag false positive — discard
+
+ # ------------------------------------------------------------------
+ # Match! Derive the private scalar for spending
+ # stealth_scalar = (spending_scalar + h_scalar) mod L
+ # ------------------------------------------------------------------
+ stealth_scalar = (spending_scalar + h_scalar) % L
+
+ results.append(MatchedAnnouncement(
+ stealth_address=derived_address,
+ ephemeral_pub_key=ann["ephemeral_pub_key"],
+ stealth_private_scalar=stealth_scalar,
+ stealth_pub_key_bytes=stealth_pub,
+ ))
+
+ return results
+```
+
+
+ The crypto needed for Wraith's Stellar scheme is deliberately small — roughly
+ 200 lines of hashing and curve arithmetic. Shipping it inline keeps this
+ quickstart dependency-free beyond `stellar-sdk` and `cryptography`, both of
+ which are already in most Python fintech backends. A standalone
+ `wraith-stellar-py` package is on the roadmap.
+
+
+---
+
+## Step 3 — Derive your stealth keys
+
+```python no-check
+import os
+from stellar_sdk import Keypair
+from wraith_crypto import STEALTH_SIGNING_MESSAGE, derive_stealth_keys, encode_meta_address
+
+# Load your Stellar keypair from the environment — never hardcode secrets
+keypair = Keypair.from_secret(os.environ["STELLAR_SECRET"])
+
+# Sign the deterministic derivation message
+# (Same message the TypeScript SDK uses — keys are cross-compatible)
+signature = keypair.sign(STEALTH_SIGNING_MESSAGE.encode())
+
+keys = derive_stealth_keys(signature)
+
+print("Spending pub key:", keys.spending_pub_key.hex())
+print("Viewing pub key:", keys.viewing_pub_key.hex())
+print("Meta-address: ", encode_meta_address(keys.spending_pub_key, keys.viewing_pub_key))
+# → st:xlm:<64 hex chars><64 hex chars>
+```
+
+The same primary keypair always produces the same stealth keys — you don't need to
+back them up separately. The `STEALTH_SIGNING_MESSAGE` constant is identical to the
+one in the TypeScript SDK, so keys derived here are usable by any SDK implementation.
+
+---
+
+## Step 4 — Fetch announcements from the Soroban RPC
+
+The Soroban RPC's `getEvents` method returns paginated contract events. The utility
+below fetches all `WRAITH_ANNOUNCEMENT` events from the announcer contract and
+normalises them into the dict shape `scan_announcements` expects.
+
+```python no-check
+import httpx # pip install httpx
+
+SOROBAN_RPC_URL = "https://rpc-futurenet.stellar.org"
+ANNOUNCER_CONTRACT_ID = "CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL" # testnet value; futurenet has no live contracts
+
+
+def _rpc(method: str, params: dict) -> dict:
+ """Minimal JSON-RPC 2.0 helper for the Soroban RPC."""
+ resp = httpx.post(
+ SOROBAN_RPC_URL,
+ json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
+ timeout=30,
+ )
+ resp.raise_for_status()
+ body = resp.json()
+ if "error" in body:
+ raise RuntimeError(f"RPC error: {body['error']}")
+ return body["result"]
+
+
+def _current_ledger_range() -> tuple[int, int]:
+ """Return (oldest_ledger, latest_ledger) from getLatestLedger."""
+ info = _rpc("getLatestLedger", {})
+ latest = info["sequence"]
+ # Soroban RPC retains ~17,280 ledgers (~24 h at 5 s/ledger)
+ oldest = max(latest - 17_000, info.get("oldestLedgerSequence", 0))
+ return oldest, latest
+
+
+def fetch_announcements() -> list[dict]:
+ """
+ Fetch all WRAITH_ANNOUNCEMENT events from the announcer contract.
+
+ Returns a list of announcement dicts with keys:
+ stealth_address, caller, ephemeral_pub_key, metadata, scheme_id
+ """
+ oldest, latest = _current_ledger_range()
+
+ result = _rpc("getEvents", {
+ "startLedger": oldest,
+ "filters": [{
+ "type": "contract",
+ "contractIds": [ANNOUNCER_CONTRACT_ID],
+ "topics": [["*", "*"]], # accept all topics from this contract
+ }],
+ "pagination": {"limit": 200},
+ })
+
+ announcements = []
+ for event in result.get("events", []):
+ # Each Soroban event has a `topic` array and a `value` (XDR ScVal)
+ # The announcer contract emits:
+ # topic[0] = Symbol("WRAITH_ANNOUNCEMENT")
+ # topic[1] = scheme_id (u32)
+ # value = Map { stealth_address, caller, ephemeral_pub_key, metadata }
+ topics = event.get("topic", [])
+ if not topics:
+ continue
+
+ # Decode the XDR value using stellar-sdk
+ from stellar_sdk import xdr as stellar_xdr
+ import base64
+
+ raw_xdr = event.get("value", {}).get("xdr", "")
+ if not raw_xdr:
+ continue
+
+ try:
+ sc_val = stellar_xdr.SCVal.from_xdr(raw_xdr)
+ except Exception:
+ continue # malformed event — skip
+
+ # Parse the map entries
+ ann: dict = {}
+ if sc_val.type == stellar_xdr.SCValType.SCV_MAP and sc_val.map:
+ for entry in sc_val.map.sc_map:
+ key_sym = entry.key.sym.sc_symbol.decode() if entry.key.sym else None
+ if key_sym == "stealth_address":
+ ann["stealth_address"] = entry.val.address.account_id # G... address
+ elif key_sym == "caller":
+ ann["caller"] = entry.val.address.account_id
+ elif key_sym == "ephemeral_pub_key":
+ ann["ephemeral_pub_key"] = entry.val.bytes.sc_bytes.hex()
+ elif key_sym == "metadata":
+ ann["metadata"] = entry.val.bytes.sc_bytes.hex()
+
+ if {"stealth_address", "ephemeral_pub_key", "metadata"} <= ann.keys():
+ announcements.append(ann)
+
+ return announcements
+```
+
+
+ Futurenet resets on demand and carries no live Wraith contracts — use the
+ testnet `ANNOUNCER_CONTRACT_ID` when testing against testnet.
+ On futurenet you can still exercise the full code path by injecting synthetic
+ announcements (see Step 6 below).
+
+
+
+ For high-throughput backends, consider caching the announcement list and
+ polling only for events newer than the last seen ledger sequence, incrementing
+ `startLedger` on each poll instead of re-fetching from `oldest`.
+
+
+---
+
+## Step 5 — Scan for owned announcements
+
+```python no-check
+import os
+from stellar_sdk import Keypair
+from wraith_crypto import (
+ STEALTH_SIGNING_MESSAGE,
+ derive_stealth_keys,
+ scan_announcements,
+)
+
+keypair = Keypair.from_secret(os.environ["STELLAR_SECRET"])
+keys = derive_stealth_keys(keypair.sign(STEALTH_SIGNING_MESSAGE.encode()))
+
+announcements = fetch_announcements() # from Step 4
+
+matches = scan_announcements(
+ announcements,
+ viewing_key=keys.viewing_key,
+ spending_pub_key=keys.spending_pub_key,
+ spending_scalar=keys.spending_scalar,
+)
+
+for m in matches:
+ print(f"Found payment at: {m.stealth_address}")
+ print(f" Private scalar: {hex(m.stealth_private_scalar)}")
+ # m.stealth_private_scalar lets you sign transactions from this stealth address
+```
+
+The scanner applies the view-tag prefilter first, so the expensive X25519 operation
+runs only for ~1 in 256 non-matching announcements. On a list of 10,000
+announcements you would expect ~39 false-positive ECDH calls before the full
+address check eliminates them.
+
+---
+
+## Step 6 — Send a stealth payment
+
+Sending has three parts: generate a stealth address for the recipient, fund it
+(or transfer a token), and publish an announcement on-chain via the Soroban
+announcer contract.
+
+### 6a — Resolve the recipient's meta-address
+
+```python no-check
+from wraith_crypto import decode_meta_address, generate_stealth_address
+
+# The recipient shares their meta-address — either directly or via a .wraith name.
+# For this quickstart we use a literal; in production resolve it from the names contract.
+RECIPIENT_META_ADDRESS = "st:xlm:"
+
+spending_pub, viewing_pub = decode_meta_address(RECIPIENT_META_ADDRESS)
+stealth = generate_stealth_address(spending_pub, viewing_pub)
+
+print("One-time stealth address:", stealth.stealth_address)
+print("Ephemeral pub key: ", stealth.ephemeral_pub_key.hex())
+print("View tag: ", stealth.view_tag)
+```
+
+### 6b — Fund the stealth address and call the announcer
+
+New Stellar accounts must be activated with `createAccount`. After activation, call
+the Soroban announcer contract to publish the ephemeral key and view tag so the
+recipient can detect their payment.
+
+```python no-check
+import os
+import base64
+from stellar_sdk import (
+ Keypair, Network, Server, TransactionBuilder, Asset,
+ Operation,
+)
+from stellar_sdk.soroban_rpc import SorobanServer
+from stellar_sdk import xdr as stellar_xdr
+
+HORIZON_URL = "https://horizon-testnet.stellar.org"
+SOROBAN_URL = "https://soroban-testnet.stellar.org"
+NETWORK_PASS = Network.TESTNET_NETWORK_PASSPHRASE
+AMOUNT_XLM = "10" # minimum to activate the new stealth account
+
+sender_keypair = Keypair.from_secret(os.environ["STELLAR_SECRET"])
+
+horizon = Server(HORIZON_URL)
+soroban = SorobanServer(SOROBAN_URL)
+
+# ------------------------------------------------------------------
+# Part 1: createAccount — activate the stealth address
+# ------------------------------------------------------------------
+sender_account = horizon.load_account(sender_keypair.public_key)
+
+activate_tx = (
+ TransactionBuilder(
+ source_account=sender_account,
+ network_passphrase=NETWORK_PASS,
+ base_fee=1000,
+ )
+ .append_create_account_op(
+ destination=stealth.stealth_address,
+ starting_balance=AMOUNT_XLM,
+ )
+ .set_timeout(30)
+ .build()
+)
+activate_tx.sign(sender_keypair)
+horizon.submit_transaction(activate_tx)
+print("Stealth account activated:", stealth.stealth_address)
+
+# ------------------------------------------------------------------
+# Part 2: invoke stealth-announcer::announce
+# ------------------------------------------------------------------
+# Build the announcement metadata: first byte = view_tag
+metadata_bytes = bytes([stealth.view_tag])
+
+# Construct the Soroban invocation arguments
+# announce(scheme_id: u32, stealth_address: Address,
+# ephemeral_pub_key: Bytes, metadata: Bytes)
+from stellar_sdk import scval
+
+invoke_args = [
+ scval.to_uint32(1), # scheme_id
+ scval.to_address(stealth.stealth_address), # stealth address
+ scval.to_bytes(stealth.ephemeral_pub_key), # ephemeral pub key
+ scval.to_bytes(metadata_bytes), # metadata (view tag)
+]
+
+sender_account = horizon.load_account(sender_keypair.public_key) # refresh sequence
+
+announce_tx = (
+ TransactionBuilder(
+ source_account=sender_account,
+ network_passphrase=NETWORK_PASS,
+ base_fee=1000,
+ )
+ .append_invoke_contract_function_op(
+ contract_id=ANNOUNCER_CONTRACT_ID,
+ function_name="announce",
+ parameters=invoke_args,
+ )
+ .set_timeout(30)
+ .build()
+)
+
+# Simulate to get resource footprint before signing
+sim = soroban.simulate_transaction(announce_tx)
+announce_tx = sim.transaction # updated with resource fee
+announce_tx.sign(sender_keypair)
+
+response = soroban.send_transaction(announce_tx)
+print("Announcement tx hash:", response.hash)
+```
+
+
+ On futurenet the Wraith contracts are not deployed, so the announcer call will
+ fail. Run Step 6 against **testnet** using the testnet `ANNOUNCER_CONTRACT_ID`
+ and the testnet Horizon/Soroban URLs from [Stellar Networks](/reference/stellar-networks).
+
+
+---
+
+## Step 7 — Spending from a stealth address
+
+To spend funds held at a stealth address you need the private scalar derived
+during scanning. Because stealth scalars are not raw seeds, the standard
+`Keypair.from_raw_ed25519_seed()` path does not work. Use `signWithScalar` from
+`wraith_crypto` below.
+
+```python no-check
+# Add to wraith_crypto.py
+
+def sign_with_scalar(
+ message: bytes,
+ scalar: int,
+ pub_key_bytes: bytes,
+) -> bytes:
+ """
+ Sign `message` using a raw ed25519 scalar (not a seed).
+
+ Required because stealth scalars are (spending_scalar + h_scalar) mod L,
+ which is not necessarily clamped and cannot be fed to standard seed-based
+ signing APIs.
+
+ Compatible with RFC 8032 ed25519 — the signature will verify against
+ `pub_key_bytes` using any standard ed25519 verifier.
+ """
+ import struct
+
+ # Encode scalar as little-endian 32 bytes
+ s_bytes = scalar.to_bytes(32, "little")
+
+ # Deterministic nonce: SHA-512(scalar_bytes || message), lower half, clamped
+ nonce_hash = _sha512(s_bytes + message)
+ r_bytes = bytearray(nonce_hash[:32])
+ _clamp_scalar(r_bytes)
+ r = int.from_bytes(r_bytes, "little")
+
+ # R = r * G
+ r_key = Ed25519PrivateKey.from_private_bytes(bytes(r_bytes))
+ R_bytes = r_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
+
+ # S = (r + SHA-512(R || pub_key || message) * s) mod L
+ k_hash = _sha512(R_bytes + pub_key_bytes + message)
+ k = int.from_bytes(k_hash, "little") % L
+ S = (r + k * scalar) % L
+
+ return R_bytes + S.to_bytes(32, "little")
+```
+
+Use it when building a withdrawal transaction from a matched stealth address:
+
+```python no-check
+from wraith_crypto import sign_with_scalar
+
+# `match` is a MatchedAnnouncement from scan_announcements()
+stealth_keypair = Keypair.from_raw_ed25519_public_key(match.stealth_pub_key_bytes)
+stealth_account = horizon.load_account(stealth_keypair.public_key)
+
+# Build a payment back to your primary account
+withdrawal_tx = (
+ TransactionBuilder(
+ source_account=stealth_account,
+ network_passphrase=NETWORK_PASS,
+ base_fee=1000,
+ )
+ .append_payment_op(
+ destination=sender_keypair.public_key,
+ asset=Asset.native(),
+ amount="9", # leave ~1 XLM for fees / base reserve
+ )
+ .set_timeout(30)
+ .build()
+)
+
+# Sign with raw scalar — not with a Keypair seed
+tx_hash = withdrawal_tx.hash()
+sig_bytes = sign_with_scalar(tx_hash, match.stealth_private_scalar, match.stealth_pub_key_bytes)
+
+withdrawal_tx.transaction.add_signature(
+ network_id=Network.TESTNET_NETWORK_PASSPHRASE,
+ public_key=stealth_keypair.public_key,
+ signature=sig_bytes,
+)
+response = horizon.submit_transaction(withdrawal_tx)
+print("Withdrawal tx hash:", response["hash"])
+```
+
+
+ For privacy, space withdrawals at least one hour apart and avoid uniform round
+ amounts. See [Privacy Best Practices](/guides/privacy-best-practices) for the
+ full scoring algorithm.
+
+
+---
+
+## Boundary: what lives in `wraith_crypto.py` vs. `stellar-sdk`
+
+| Responsibility | Handled by |
+|---|---|
+| SHA-256 domain separation | `wraith_crypto.py` |
+| Scalar clamping, ed25519 point addition | `wraith_crypto.py` |
+| X25519 ECDH (via `cryptography`) | `wraith_crypto.py` |
+| StrKey encoding (`G...` addresses) | `stellar-sdk` |
+| Transaction building, XDR encoding | `stellar-sdk` |
+| Soroban RPC calls | `stellar-sdk` + `httpx` |
+| Account management, sequence numbers | `stellar-sdk` Horizon |
+
+The `wraith_crypto.py` file is intentionally kept free of Soroban or Horizon
+concerns — it handles only the cryptographic layer so it stays testable in isolation.
+
+---
+
+## Environment variables
+
+```bash no-check
+# .env — never commit this file
+STELLAR_SECRET=S...your-secret-key
+
+# Target testnet for live Wraith contracts
+STELLAR_NETWORK=testnet
+STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
+STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
+STELLAR_RPC_URL=https://soroban-testnet.stellar.org
+STELLAR_ANNOUNCER_CONTRACT_ID=CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL
+```
+
+---
+
+## Next steps
+
+
+
+ Full reference for the TypeScript SDK counterparts — useful if your backend calls a Node.js service alongside Python
+
+
+ Passphrase, RPC URLs, contract IDs, and Friendbot for testnet, futurenet, and mainnet
+
+
+ Design rationale behind X25519 ECDH, view-tag derivation, and RFC 8032 scalar signing
+
+
+ Withdrawal timing, amount rounding, and the full privacy score algorithm
+
+
diff --git a/sdk/overview.mdx b/sdk/overview.mdx
index c7c644d..bd815c1 100644
--- a/sdk/overview.mdx
+++ b/sdk/overview.mdx
@@ -171,4 +171,12 @@ Both ESM and CJS formats are supported. TypeScript declarations are included.
- [Stellar Crypto Primitives](chains/stellar) — low-level stealth address functions for Stellar
- [Solana Crypto Primitives](chains/solana) — low-level stealth address functions for Solana
- [CKB Crypto Primitives](chains/ckb) — low-level stealth address functions for Nervos CKB
+
+## Using Wraith from Python
+
+If your backend runs Python rather than Node.js, the TypeScript SDK is not required.
+The [Python Quickstart](/guides/quickstarts/python) shows how to derive stealth keys,
+fetch announcements from the Soroban RPC, scan for owned payments, and send a stealth
+transfer — all with `stellar-sdk` and a small inline crypto utility, no TypeScript
+toolchain needed.
- [Rust Quickstart](/guides/quickstarts/rust) — build a Rust backend or CLI that talks to the Stellar contracts directly