TRANSACTIONS

Transaction
Architecture

UTXO-based type model — no scripting engine

01 — CURRENT

What Works Today

P2PKH Payments
WALLET + CLI

Single-key ECDSA secp256k1 with compressed pubkeys and mandatory LOW-S enforcement. BIP143-simplified sighash with genesis hash for replay protection.

Fee Market
WALLET + CLI

Rational fee-rate ordering (fee/size as integer ratio, no floating point). MIN_RELAY_FEE: 1 stock/byte (static). Dynamic relay floor activates at block 10,000 — scales with mempool pressure (2x/5x/10x/25x, ceiling 50 stocks/byte). Policy-only, not consensus. Mempool prioritizes by fee-rate.

UTXO Model
WALLET + CLI

Full UTXO tracking with ConnectBlock/DisconnectBlock for reorg support. BlockUndo preserves spent outputs for chain reorganization.

Batch Transactions
WALLET + CLI

Up to 256 inputs and 256 outputs per transaction (consensus). Policy limits: 128 inputs, 32 outputs.

Coinbase
IMMUTABLE

1000-block maturity (~7 days). Constitutional 50/25/25 split: miner, Gold Vault, PoPC Pool. Immutable at genesis.

Consensus Limits
PARAMETERS
MAX_BLOCK_BYTES1,000,000
MAX_TX_BYTES100,000 consensus / 16,000 policy
COINBASE_MATURITY1000 blocks
MIN_RELAY_FEE1 stock/byte
DUST_THRESHOLD10,000 stocks
MAX_INPUTS256 consensus / 128 policy
MAX_OUTPUTS256 consensus / 32 policy
02 — ACTIVATION

Activating at Block 5000

CONSENSUS UPGRADE
BOND_LOCK (OUT_BOND_LOCK, 0x10)
CONSENSUS

Time-locked output. Funds locked until specified block height. 8-byte payload: lock_until as uint64_t LE. Used by PoPC Model B for custody bonds.

ESCROW_LOCK (OUT_ESCROW_LOCK, 0x11)
CONSENSUS

Escrow with designated beneficiary. 28-byte payload: lock_until[8] + beneficiary_pkh[20]. Available for PoPC contracts and future application-layer services.

Capsule Protocol v1
OVERLAY

Structured metadata in OUT_TRANSFER outputs. 12-byte header + up to 243-byte body. Types: open notes, sealed notes, document references, certificate instructions. Encryption: ECIES-secp256k1-AES256-GCM or X25519-AES256-GCM.

03 — SENDING

Sending Capsule Transactions

OVERLAY · PUBLIC MODES

Capsules are public-mode metadata attached to OUT_TRANSFER outputs and validated by the mempool, not consensus. There are two ways to send a capsule transaction today: the sost-cli binary and the web wallet (sost-wallet.html). Both produce the same on-chain bytes; the cli is appropriate when you want multi-key wallets, file-based document references, or shell automation.

Picking the right RPC endpoint
CONNECTIVITY

Both the cli and the web wallet need an RPC endpoint to broadcast. There are three correct ones depending on where the node lives. The most common confusion is treating 127.0.0.1 as if it pointed at sostcore.com — it does not. 127.0.0.1 always means this exact computer (the one running the cli or the browser), never the SOST website server.

http://127.0.0.1:18232
LOCAL NODE
You ran sost-node on this machine and want full access (read + broadcast). The full URL is exactly http://127.0.0.1:18232; 18232 is the RPC port and there is nothing else to append. Auth comes from your own sost-node.conf.

Do not use this if you are only browsing the website — on a normal user's computer it points at their machine, which has no SOST node.
/rpc/public
WEBSITE · READ ONLY
The sostcore.com proxy. Reads balances, blocks, mempool, address UTXOs — all the data the explorer needs. Cannot broadcast; the web wallet refuses to sign + send when this is the active endpoint and the cli will not accept it for sends either.
/rpc
WEBSITE · AUTH
The authenticated endpoint on sostcore.com. Requires the RPC user + password the operator gave you. Allows broadcast.

The cli takes the endpoint via --rpc <host:port> + --rpc-user + --rpc-pass. The web wallet shows it in the top RPC bar (the ? button next to CONNECT opens the same matrix). A coloured badge under CONNECT labels the active endpoint (LOCAL NODE / READ ONLY / AUTH REQUIRED / CUSTOM) so the mode is never silent.

Operator self-test (run on the VPS):

source /etc/sost/rpc.env

# Direct to the node — must reject "00" with -22 (TX decode), not -401.
curl -s -u "$RPC_USER:$RPC_PASS" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"sendrawtransaction","params":["00"]}' \
  http://127.0.0.1:18232/ | jq .

# Through nginx /rpc — same expected error code.
curl -s -u "$RPC_USER:$RPC_PASS" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"sendrawtransaction","params":["00"]}' \
  http://127.0.0.1/rpc | jq .
A. sost-cli send — with capsule
CLI

Canonical APP-rewards send. Replace the recipient address, amount, and capsule text with your own. Global flags can appear in any order (parser fix landed in 6c1338e).

./sost-cli send sost1a8eae8f80fedd8d86187db628a0d81e0367f76de 0.01 \
  --wallet phase2-miner-wallet.json \
  --from-label phase2-miner \
  --rpc 127.0.0.1:18232 \
  --rpc-user <your-rpc-user> \
  --rpc-pass <your-rpc-pass> \
  --capsule-mode structured \
  --capsule-template payment_receipt_v1 \
  --capsule-text 'category=APP rewards distribution; ref=batch-001; period=2026-05; note=verified settlement'

Before broadcast the cli prints Capsule attached: mode=structured, payload=N bytes. If you do not see that line, the mode flag did not reach the parser — pull to 6c1338e or later.

--from-label is a wallet alias, not an SbPoW tag
CLARIFICATION

Two distinct concepts that share the word "label" in some contexts:

SbPoWSurface-bound proof-of-work — the algorithm a miner uses to commit a block. It has no per-transaction tag, no flag, and is not selected when sending a transfer. SbPoW only matters at sost-miner time.
--from-labelA human alias for a key inside your wallet json. The label is whatever you typed when the key was created (phase2-miner, treasury, app-rewards, etc.). The cli uses it to pick which key signs and which address change returns to.

Sending a transfer never activates SbPoW. SbPoW activates when a miner builds a candidate block.

Find or create a wallet label
CLI

List every key+label pair in a wallet:

./sost-cli --wallet phase2-miner-wallet.json listaddresses

The output prints one row per key with label + address + balance. To filter just the (label, address) pairs as JSON for scripting:

jq -r '.keys[] | [.label, .address] | @tsv' phase2-miner-wallet.json

Create a new key under a chosen label:

./sost-cli --wallet wallet.json getnewaddress app-rewards
Source-account selection — which flag to use
CLI
no flagSpends from wallet.default_address() (the first key in the file). On a single-key wallet this is what you want.
--from-label <name>Resolves to whichever key carries that label. Best for multi-key wallets where keys have stable roles (treasury, phase2-miner, etc.).
--from-address <sost1...>Spends from the exact bech32 address. Best for scripts that already know the address and do not care about labels.

The two flags are mutually exclusive. On a multi-key wallet without either flag the cli prints a one-line notice pointing at them so the default-key fallback is never silent.

Capsule types (SCPv1) — what each one carries
OVERLAY

Every capsule starts with the same 12-byte header (magic 'SC' + version 1 + type + flags + template_id + locator_type + hash_alg + enc_alg + body_len(u8) + reserved(2)). The body shape depends on the type. SCPv1 defines seven standard types; sealed variants share the body of their open counterpart but encrypt it under ECIES-secp256k1 + AES-256-GCM. Total payload (header + body) caps at 255 bytes for the sighash path, with consensus accepting up to 512.

0x01 — OPEN_NOTE_INLINE
WALLET + CLI

Plain ASCII/UTF-8 message visible to anyone reading the chain. The simplest capsule type. Common use: human-readable receipts, public memos, signed acknowledgements.

body layouttext_len(1) + text(N)
max body242 bytes (header + body fits in the 255-byte sighash window)
policy cap80 bytes (mempool standardness)
CLI--capsule-mode open-note --capsule-text "Public memo"
web walletMessage Type → Open Note
0x02 — SEALED_NOTE_INLINE
PLANNED

Same intent as open-note but the body is encrypted to one or more recipient pubkeys via ECIES-secp256k1 + AES-256-GCM. Body is opaque to anyone without a private key. Not in the current release; the cli rejects it with a clear error pointing at the future ECIES commit.

body layoutrecipient_count(1) + (recipient_pubkey(33) + ephemeral(33) + iv(12) + ciphertext_len(1) + ciphertext(N) + tag(16)) repeated
statusdeferred — ECIES wiring not in this release
0x03 — DOC_REF_OPEN
CLI ONLY

A pointer to an off-chain document. The body carries an unforgeable cryptographic anchor (file SHA-256) plus a free-text locator (IPFS CID, HTTPS URL, etc.) so anyone can fetch the file and verify the hash matches. Use for contracts, invoices, audit reports.

body layoutcapsule_id(8 LE) + file_size(4 LE) + file_hash(32) + manifest_hash(32) + locator_len(1) + locator_ref(N)
min body77 bytes; max body 243 bytes
CLI--capsule-mode doc-ref --capsule-file ./contract.pdf --capsule-locator ipfs://Qm...
locatorcli auto-detects ipfs:// vs https://; manifest_hash stays zero unless you supply a pinning manifest
web walletdeferred — needs file picker UI; use cli for now
0x04 — DOC_REF_SEALED
PLANNED

Encrypted variant of DOC_REF_OPEN. The hash + locator are sealed so only the recipient(s) can identify which document the TX refers to. Useful when the very fact that a particular document is referenced is sensitive. Same ECIES dependency as 0x02.

statusdeferred — ECIES wiring not in this release
0x05 — TEMPLATE_FIELDS_OPEN
WALLET + CLI

Structured key=value record indexed by a TemplateId so explorers and downstream tools can decode it deterministically. The canonical APP-rewards send uses payment_receipt_v1 with fields like category=...; ref=...; period=....

body layoutcapsule_id(8 LE) + field_codec(1) + fields_len(1) + fields(N)
codec0x00 = ASCII (v1 standard); reserved values for future codings
templates 0x01 invoice_v1 · 0x02 contract_ref_v1 · 0x03 payment_receipt_v1 · 0x04 transfer_instruction_v1 · 0x05 escrow_note_v1 · 0x06 compliance_record_v1 · 0x07 warranty_record_v1 · 0x08 shipment_record_v1 · 0x09 gold_cert_note_v1 · 0x0A custom_kv_v1
CLI--capsule-mode structured --capsule-template payment_receipt_v1 --capsule-text "category=APP rewards; ref=batch-001; period=2026-05"
web walletMessage Type → Structured Data → pick template → type fields
0x06 — TEMPLATE_FIELDS_SEALED
PLANNED

Structured fields encrypted to recipient(s). The template_id stays in the clear so the recipient knows which schema to apply after decryption; the field bytes themselves are sealed. Same ECIES dependency.

statusdeferred — ECIES wiring not in this release
0x07 — CERT_INSTRUCTION
WALLET + CLI

A certification or authority instruction: signed-on-chain attestation that some external object (a gold certificate, a compliance ruling, an authority decision) applies to the recipient. Carries a kind/instr pair, a numeric reference, optional expiry, and an optional human note.

body layoutcert_kind(1) + instr_kind(1) + cert_id(8 LE) + ref_value(8 LE) + expires_at(4 LE) + note_len(1) + note(N)
min body23 bytes (note empty); max note 64 bytes by policy
CLI--capsule-mode cert --capsule-text "<note>" (cli currently sets cert_kind=instr_kind=1; future flags will expose more)
web walletMessage Type → Certification

Type codes 0x080x7F are reserved for future SOST extensions; 0x800xFF are experimental/local and not relayed by default.

Send mechanics — what runs under the hood
WALLET INTERNALS

The user-facing flow is a single click on Send. Underneath, both the web wallet and sost-cli run a small pipeline that handles fee convergence, double-spend avoidance, oversized-tx splitting, and broadcast safety. These are not protocol changes — nothing here touches consensus, the sighash, the RPC schema or the transaction wire format. They are wallet ergonomics built on top of the same primitives a hand-built tx would use.

1 — Fee-pass converging loop

Pre-estimating the byte size of a tx that has not been signed yet is unreliable: the input set the wallet picks depends on the fee, which depends on the size, which depends on the input set. The wallet runs the loop instead. Pass 1 builds with a small seed fee (1 input + 2 outputs + capsule body), reads rawTx.length / 2 from the signed result, recomputes fee = txSize × feePerByte and rebuilds if anything changed. Up to 4 passes plus a final exact-fee rebuild so the confirm dialog and the broadcast see byte- equal numbers. If the loop cannot converge, the wallet refuses to broadcast (the node would reject with consensus rule S8 anyway). Same logic in sost-cli send and sost-cli createtx.

2 — In-flight UTXO tracking (web)

The web wallet picks UTXOs smallest-first greedy. Without state, two clicks on Send within seconds would pick the same inputs and the second broadcast would fail with the node's double-spend reject. State here: Map<"txid:vout", {ts, broadcast_txid, address}> persisted in localStorage with a 5-min TTL, scoped per wallet address. Reservation timing is critical — UTXOs are reserved before sendrawtransaction so a concurrent build cannot re-pick them; on RPC failure the reservation releases immediately and the user can retry. A refreshBalance reconcile drops reservations whose UTXOs the node no longer reports.

3 — Auto-split capsule payment (> 16 KB)

When a single-recipient capsule send converges to a signed tx larger than the 16 000-byte mempool standardness cap (typical case: hundreds of small UTXOs, ~22 KB once a capsule is attached), the wallet offers to break the amount into N smaller transactions to the same recipient, each carrying the same capsule, broadcast sequentially. Default chunk size 150 SOST; minimum 5 SOST as a "no-dust-tail" floor. The chunk-sum invariant is enforced: a reduce()-based check throws if chunks fail to add up to the requested amount, and a self-test runs once at script load against eight reference cases (650 / 500 / 300 / 150 / 149 / 5 / 1 / 301) to catch regressions before the user can hit Send. Per-chunk: refresh + reconcile + filter in-flight + fee converge + reserve + broadcast + record txid; on failure the loop stops and the report shows which chunks went out and which did not.

4 — Multi-recipient sends

Adding more than one recipient row routes the tx to sendmany: one transaction with N payment outputs + one change output, atomic broadcast, capped at 100 000 bytes consensus and 16 000 bytes policy. Multi-recipient + capsule is rejected by design — the V13 contract is "one capsule per single-recipient tx" so the auto-split flow is the canonical answer to "send the same capsule to the same address in many parts". For different capsules to different addresses, send them as separate single-recipient transactions. The sendmany path uses the same converging fee loop and the same in-flight reservation as single- recipient send.

5 — Sealed Capsule decrypt

The web wallet dashboard ships a Decrypt Sealed Capsule card. Paste a txid; the wallet calls gettransaction, walks every output's payload_hex, filters those carrying a sealed type (0x02 / 0x04 / 0x06) and tries to open each envelope with the wallet's active privkey. On success a gold callout shows the type, template (for sealed structured), and the recovered plaintext (note text, structured fields, or doc-ref body). On failure the chip reads "not decryptable by this wallet". Plaintext lives in DOM only — lockWallet wipes it and nothing is ever persisted to localStorage / sessionStorage / console. The CLI mirrors this with sost-cli capsule-decrypt <txid>.

6 — Endpoint awareness + re-entrancy guard

The RPC bar at the top of the wallet labels the active endpoint as LOCAL NODE, READ ONLY or AUTH REQUIRED. A read-only endpoint (/rpc/public) cannot accept a broadcast, so the wallet refuses to sign + send before any key material is exercised — failing fast instead of after a confusing 5-minute timeout from nginx. A window._sendInProgress flag, cleared in an outer finally, blocks a double-click from spawning a parallel async build; the second click reads "A transaction is already being broadcast. Wait for it to finish."

7 — Explorer lazy-load history

The explorer's Latest Transactions table loads the last 30 blocks plus up to 200 transfers from the listtransfers RPC. When the user clicks OLDER → past the local last page the button changes to LOAD OLDER → and a click pulls the next 30 blocks from the chain in one batch (getblockhash + getblock + gettransaction per non-coinbase entry). The cursor walks backwards indefinitely; once block 0 is included the button reads GENESIS REACHED. The 30-second auto-refresh only re-scans the head; older rows already pulled in stay in the table.

Wallet-side reference walk-through: sost-wallet.html (open the Send card, expand the Mode guide ▾ button, then Send mechanics ▾). Fee / TXfields / convergence loop and the auto-split self-test live inline in website/sost-wallet.html; they have no server-side dependency.

B. Web wallet — same capsule, no shell
UI

Open sost-wallet.html, import or unlock the wallet that holds the funds, then:

  1. Recipient: paste the destination address.
  2. Amount: 0.01 (or whatever you intend to send).
  3. Message Type: pick Structured Data, Open Note, or Certification.
  4. Template (Structured only): the default payment_receipt_v1 covers most cases.
  5. Text: the capsule body. For payment receipts:
    category=APP rewards distribution; ref=batch-001; period=2026-05; note=...
  6. Send. The wallet pre-validates the capsule before signing and binds it into the sighash, so any tampering on the broadcast path triggers a node-side signature failure (safe-by-rejection).

Sealed-* and document-reference modes still bridge to the cli (the web wallet does not have a file picker yet). For a single-key wallet there is no per-key selector — the active session uses whichever key is loaded.

Verifying the capsule landed
EXPLORER

Once the broadcast confirms, the explorer surfaces the capsule in two places:

  • Latest Transactions table — the new CAPSULE column reads e.g. structured · receipt.
  • TX detail panel — three rows (capsule type + body length, template, decoded text) below inputs/outputs.

The same data is available over RPC: gettransaction returns a top-level capsule object plus per-output payload_hex; listtransfers returns the summary inline per row. No extra index, no extra daemon — the payload bytes already live in the block.

04 — FUTURE

Future Research

Multisig (M-of-N)
Fixed output type OUT_MULTISIG without full scripting. Medium complexity.
Relative Timelocks (CSV-like)
Enables payment channels. Requires sequence number field. Medium complexity.
HTLC
Hash timelock contracts for Lightning compatibility. High complexity.
RBF / CPFP
Mempool-only changes, no consensus change needed. Low complexity.
Schnorr Signatures
Key aggregation, batch verification. Could coexist with ECDSA. Medium complexity.

These features represent potential future upgrades to the SOST transaction architecture. Implementation will follow the type-based activation pattern established by BOND_LOCK and ESCROW_LOCK. No timeline commitments are made. Each upgrade requires a consensus change and will only be deployed after thorough testing, formal verification where applicable, and community review. SOST prioritizes correctness and security over feature velocity.

05 — PHILOSOPHY

Design Philosophy

Bitcoin Script vs SOST Types
COMPARISON
Aspect Bitcoin (Script) SOST (Types)
Model Stack-based scripting Fixed output types
Flexibility Arbitrary programs One type per capability
Attack surface Large (script injection, witness malleability) Minimal (no interpreter)
Validation speed Variable (script execution) Fast (type switch)
New features Soft fork (new opcodes) Hard fork (new output type)
Smart contracts Limited (Bitcoin Script Turing-incomplete) None (not a goal)

SOST chose a type-based model to minimize attack surface and maximize validation speed. Each transaction capability is a distinct output type with consensus-defined semantics. New capabilities activate at predetermined block heights, following the pattern established by BOND_LOCK at height 5000. This design trades flexibility for safety — SOST is a payment chain with constitutional reserves, not a smart contract platform.

🎮