UTXO-based type model — no scripting engine
Single-key ECDSA secp256k1 with compressed pubkeys and mandatory LOW-S enforcement. BIP143-simplified sighash with genesis hash for replay protection.
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.
Full UTXO tracking with ConnectBlock/DisconnectBlock for reorg support. BlockUndo preserves spent outputs for chain reorganization.
Up to 256 inputs and 256 outputs per transaction (consensus). Policy limits: 128 inputs, 32 outputs.
1000-block maturity (~7 days). Constitutional 50/25/25 split: miner, Gold Vault, PoPC Pool. Immutable at genesis.
| MAX_BLOCK_BYTES | 1,000,000 |
| MAX_TX_BYTES | 100,000 consensus / 16,000 policy |
| COINBASE_MATURITY | 1000 blocks |
| MIN_RELAY_FEE | 1 stock/byte |
| DUST_THRESHOLD | 10,000 stocks |
| MAX_INPUTS | 256 consensus / 128 policy |
| MAX_OUTPUTS | 256 consensus / 32 policy |
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 with designated beneficiary. 28-byte payload: lock_until[8] + beneficiary_pkh[20]. Available for PoPC contracts and future application-layer services.
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.
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.
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 .
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.
Two distinct concepts that share the word "label" in some contexts:
| SbPoW | Surface-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-label | A 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.
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
| no flag | Spends 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.
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.
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 layout | text_len(1) + text(N) |
| max body | 242 bytes (header + body fits in the 255-byte sighash window) |
| policy cap | 80 bytes (mempool standardness) |
| CLI | --capsule-mode open-note --capsule-text "Public memo" |
| web wallet | Message Type → Open Note |
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 layout | recipient_count(1) + (recipient_pubkey(33) + ephemeral(33) + iv(12) + ciphertext_len(1) + ciphertext(N) + tag(16)) repeated |
| status | deferred — ECIES wiring not in this release |
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 layout | capsule_id(8 LE) + file_size(4 LE) + file_hash(32) + manifest_hash(32) + locator_len(1) + locator_ref(N) |
| min body | 77 bytes; max body 243 bytes |
| CLI | --capsule-mode doc-ref --capsule-file ./contract.pdf --capsule-locator ipfs://Qm... |
| locator | cli auto-detects ipfs:// vs https://; manifest_hash stays zero unless you supply a pinning manifest |
| web wallet | deferred — needs file picker UI; use cli for now |
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.
| status | deferred — ECIES wiring not in this release |
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 layout | capsule_id(8 LE) + field_codec(1) + fields_len(1) + fields(N) |
| codec | 0x00 = 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 wallet | Message Type → Structured Data → pick template → type fields |
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.
| status | deferred — ECIES wiring not in this release |
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 layout | cert_kind(1) + instr_kind(1) + cert_id(8 LE) + ref_value(8 LE) + expires_at(4 LE) + note_len(1) + note(N) |
| min body | 23 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 wallet | Message Type → Certification |
Type codes 0x08–0x7F are reserved for
future SOST extensions; 0x80–0xFF
are experimental/local and not relayed by default.
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.
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.
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.
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.
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.
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>.
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."
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.
Open sost-wallet.html, import or unlock the wallet that holds the funds, then:
0.01 (or whatever you intend to send).Structured Data, Open Note, or Certification.payment_receipt_v1 covers most cases.category=APP rewards distribution; ref=batch-001; period=2026-05; note=...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.
Once the broadcast confirms, the explorer surfaces the capsule in two places:
CAPSULE
column reads e.g. structured · receipt.
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.
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.
| 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.