PayFi Public Ledger — Specification Ledger format: payfi-ledger/1 Export format: payfi-ledger-export/1 Verifier: payfi-verify.js 2.6.2 Revisions (older entries are unchanged and still valid): 2 (9 Oct 2026) fee routing (fee_to, owed_to), official domains, batches of public events (Merkle roots), snapshots 3 (9 Oct 2026) PayFi ID: member numbers, names, linked wallets, transfers (section 9) 4 (9 Oct 2026) PayFi Meet record: marks and blocks (section 10) 5 (10 Oct 2026) buyer wallet on settlements (section 11) 6 (10 Oct 2026) PayFi Meet remote transfer mode: seller collateral, neutral stalemate mark, "mode" on remote trades (section 12) 7 (10 Oct 2026) auctions of reserved member numbers and short names (section 13) 8 (10 Oct 2026) physical goods orders and verified reviews (section 14) 1. Purpose PayFi is a payment-link service for USDT and USDC on Solana. Every payout it makes is written into an append-only public ledger. This document describes the ledger precisely enough that anyone can check it with their own code, without trusting PayFi, and states plainly what the checks do and do not prove. Files (all public): https://payfi.cx/ledger/ the page (runs payfi-verify.js in your browser) https://payfi.cx/ledger/ledger.json the ledger (export format, section 3) https://payfi.cx/ledger/payfi-verify.js the verifier (browser + Node.js, no dependencies) https://payfi.cx/ledger/spec.txt this document 2. Entries and the hash chain The ledger is a sequence of entries numbered 1, 2, 3, ... Each entry is { seq, prev, hash, body } seq integer, starting at 1, no gaps prev hash of entry seq-1; for entry 1 it is 64 zeros body a JSON object; body.kind says what the entry is (section 4) hash lowercase hex SHA-256 of canonical({ "seq": seq, "prev": prev, "body": body }) Canonical JSON: - object keys sorted (JavaScript default string sort, i.e. UTF-16 code units), keys with undefined values omitted; - no whitespace; strings and keys encoded exactly as JSON.stringify does; - numbers are only safe integers (|n| < 2^53); money is never a number; - arrays keep their order; null, true, false as usual. Because each hash covers the previous hash, changing any entry changes every hash after it. Entry 1 must be the genesis. Money: integer strings in base units. USDT and USDC have 6 decimals ("1500000" = 1.5); SOL is in lamports (9 decimals). PayFi treats 1 USDT = 1 USDC. Order reference: an order is never named in the ledger. It appears as order = SHA-256( "payfi-ledger/1|order|" + SHA-256(token) ) (hex, UTF-8) where token is the secret part of the buyer's order link https://payfi.cx/pay#order=. Only the holder of the link can compute it. Never in the ledger: product names, delivery content, e-mail, IP addresses, order link tokens, keys. 3. Export file (ledger.json) { "format": "payfi-ledger-export/1", "generated_at": , "head": { "seq": , "hash": }, "entries": [ { "seq", "prev", "hash", "body" }, ... ], in order "proofs": [ { "covers_seq", "covers_hash", "status", "bitcoin_height", "ots" }, ... ], "verifier": "https://payfi.cx/ledger/payfi-verify.js", "spec": "https://payfi.cx/ledger/spec.txt" } "ots" is a base64 OpenTimestamps proof file (section 6). "status" and "bitcoin_height" are only hints from PayFi; the verifier ignores them and reads the proof itself. The server checks every 10 minutes and rewrites the file when something changed, and at least once an hour in any case (so "generated_at" shows the file is still maintained). It can be up to ~10 minutes behind. 4. Entry kinds (body) All bodies have "kind" and "at" (ms, UTC, when the event happened). genesis (entry 1 only) format "payfi-ledger/1" text one sentence: a dated newspaper headline (date marker only; no affiliation), the Solana slot and blockhash, and the number of payments settled since launch headline { source, date, title, url, archive } solana { cluster: "mainnet-beta", commitment: "finalized", slot, blockhash } launch ISO time of the public launch (2026-10-01T00:00:00.000Z) settled_since_launch number of settlements between launch and the genesis platform_wallet PayFi's Solana wallet: it signs and pays the fee of every payout and receives PayFi's fee code, order_ref, amounts descriptive settlement — one paid order paid out on Solana in ONE atomic transaction order order reference (section 2) price the price of the order in everything taken from the order's deposit address (= seller + platform) seller sent to the seller seller_usdc of which in USDC (null on old entries) platform sent to PayFi platform_usdc of which in USDC owed overpayment included in "platform" and held for the buyer (owed_usdt, owed_usdc: per token) payout_wallet seller's wallet; deposit_address the order's deposit address buyer_wallet (revision 5, optional) the wallet that paid the order, read from the blockchain: the owner of the token account that sent USDT/USDC to the deposit address. Anyone can confirm it in the deposit address history. tx the Solana transaction signature backfilled true if the payout happened before the ledger existed fee_to (revision 2) per token, the wallet that received PayFi's part "platform" - "owed" (fee + setup fee): { "USDT": wallet, "USDC": wallet } owed_to (revision 2) per token, the wallet that holds the overpayment "owed" (normally platform_wallet, which pays overpayment refunds) Entries without fee_to/owed_to were written before revision 2: there, all of "platform" went to one PayFi wallet. Rules: in = seller + platform; owed <= platform; seller + platform - owed = price refund — money returned to a buyer order, source ("deposit" | "platform"), asset ("USDT" | "USDC" | "SOL"; SOL only for SOL sent by mistake to an order address), amount (received by the buyer), fee, recipient, tx meet — one PayFi Meet trade (USDT/USDC swapped for cash face to face) whose escrow was emptied on Solana in ONE atomic transaction trade trade number; outcome "release" (to the buyer) | "refund" (back to the seller) asset "USDT" | "USDC"; amount the trade amount in everything the trade's escrow held buyer sent to the buyer; fee sent to PayFi; seller_back returned to the seller buyer_wallet, seller_wallet, escrow (the trade's escrow address), tx fee_to (revision 2, optional) the wallet that received the fee, when it is not platform_wallet (PayFi's cold fee wallet) Never recorded: names, home or meeting address, bank, meeting time, cash price, chat, descriptions. Rules: in = buyer + fee + seller_back; release: buyer + fee = amount (anything above "amount" goes back to the seller); refund: buyer = 0 and fee = 0 anchor — a Bitcoin timestamp of the chain head method "opentimestamps", covers { seq, hash } (an earlier entry), calendars [urls] release — fingerprint of the server code that was started: code, files config — the money rules in force (fee in basis points, minimum price, ...): rules rules.fee_cold_wallet (revision 2): PayFi's cold fee wallet. Its key is never on the PayFi server; fees are sent there in the payout itself. rules.fee_backup_wallet (revision 2): the backup fee wallet, used only while the issuer has frozen a PayFi token account. The verifier FAILS any fee recorded as going to a wallet that is not platform_wallet or a fee wallet declared in an earlier config entry, and any overpayment held outside PayFi's wallets (a declared fee wallet is a warning: it happens only while an account is frozen). note — a public operator note (corrections, explanations): text, ref Revision 2 kinds: domains — the official PayFi web addresses: domains [host names]. The latest entry is in force. An address not listed is not PayFi. batch — many public events of one topic, recorded as ONE Merkle root topic (e.g. "forum"), from, to (ms, UTC: the period), count, root The events themselves are not in the ledger. For each event: leaf = SHA-256("payfi-ledger/1|leaf|" + canonical(event)) node = SHA-256("payfi-ledger/1|node|" + left_hex + right_hex) leaves in the order PayFi recorded them; an odd node at the end of a level is paired with itself. Proof for an event: GET https://payfi.cx/api/ledger/proof/ returns { event, batch (seq), index, count, root, path: [{ side: "L"|"R", hash }] }; payfi-verify.js exports merkleLeaf() and verifyMerkleProof() to check it. Events never contain chat text, identification details, real names, places, bank accounts, e-mail, IP addresses or keys; private items appear only as a fingerprint of their encrypted form. snapshot — a periodic snapshot (e.g. weekly ranks): topic, period, count, root (Merkle root of all rows, same rules as batch, null when count is 0), top (the first rows, readable). Topic "ranks" (weekly, period = ISO week such as 2026-W42): one row per numbered PayFi member, sorted by trades (most first, then lowest number): { number, name, rank, trades, buyers, lintasan, laut }. lintasan and laut are left out when the member hides them; laut appears once wealth levels are public. laut = level of the 60-day average of USDT + USDC + SOL in the member's own and linked wallets (standard token accounts), read from Solana every hour, SOL priced by Pyth SOL/USD at the same reading; days before the readings started and hours before a wallet was linked count as zero. Levels: plankton, shrimp 100, crab 1,000, octopus 5,000, fish 10,000, dolphin 50,000, shark 100,000, whale 1,000,000, humpback 10,000,000 USD. All rows: GET https://payfi.cx/api/passport-snapshot/; the Merkle root of merkleLeaf(row) over all rows, in that order, must equal the root here. Ranks are counted from this ledger only: trades = paid-out sales (at most 20 a day) + PayFi Meet releases (one counterpart counts at most twice); buyers = different wallets that paid the member's sales (buyer_wallet, revision 5) or traded with the member on Meet, plus, for older sales without a buyer_wallet, at most one per day with sales; sales paid from the member's own or linked wallets do not count at all; time = since the member entry. A "not_released" mark holds the rank for 180 days. Topic "posts" (weekly, same period format): one row per PayFi Forum name with at least one post: { name, number (when the name has a PayFi number), threads, replies }, counting posts that are not deleted, without blocked accounts, sorted by threads + replies (most first, then name). All rows: GET https://payfi.cx/api/passport-snapshot/?topic=posts In use since revision 3 (section 9): reserve_plan, reserve_numbers, member, link_wallet, transfer_identity. Since revision 4 (section 10): mark, block. Since revision 6 (section 12): bond. Since revision 7 (section 13): auction, auction_number, auction_name. Since revision 8 (section 14): ship, ship_review. Reserved for later revisions (each will be specified here before it is used): moderation (reputation and operator); lock, raise, revenue_share (money). Corrections are always new entries; nothing is ever edited or deleted. 5. What the verifier checks Offline (no network): a. Hash chain: seq without gaps, prev links, every hash recomputed; entry 1 is genesis; every body has an integer "at". A warning if "generated_at" is more than 2 hours old (the server stopped refreshing the file) or in the future. b. Genesis: fields well-formed; its sentence contains its slot and blockhash; settled_since_launch equals the number of settlement entries dated from the launch up to the genesis. c. Every settlement adds up (rules above); every refund is well-formed; no transaction appears twice; no order is paid out twice. d. Every anchor: covers.hash equals the hash of entry covers.seq; its proof file exists, parses, and commits to exactly that hash. d2. Revision 2: every domains, batch and snapshot entry is well-formed (host names, 64-hex Merkle roots, positive counts). The latest domains entry and the cold fee wallet (from the latest config) are reported. Online, against Solana (default https://api.mainnet-beta.solana.com; any RPC with --rpc): e. getBlock(genesis slot): blockhash must equal the genesis blockhash, and the block time must not be later than the genesis (60 s clock tolerance). Since a blockhash cannot be known in advance, the ledger did not exist before that block. f. getTransaction(tx) for every settlement and refund (finalized, jsonParsed): it must exist and have succeeded, and its fee payer (first signer) must be platform_wallet — so only PayFi's own transactions can be cited. For a settlement, deposit_address must also have signed. From the USDT/USDC token balance changes (preTokenBalances / postTokenBalances, grouped by owner): deposit_address lost exactly "in"; payout_wallet gained exactly "seller" (and "seller_usdc" in USDC); platform_wallet gained exactly "platform" (and "platform_usdc"); no other wallet gained or lost, except: PayFi's fee may go, per token, to ONE other wallet (the backup fee wallet, used only if the issuer froze PayFi's main token account); then main + backup must equal "platform" exactly, and this is a warning. Revision 2 settlements (with fee_to/owed_to) are checked exactly, per token: payout_wallet gained the seller part, fee_to[token] gained platform - owed, owed_to[token] gained owed, and nobody else moved. For refunds: recipient gained exactly "amount" of "asset" (SOL from meta.preBalances / postBalances). For USDT/USDC refunds, a warning if the other movements do not add up (payer lost amount + fee, or just amount when paid from PayFi's own wallet; other wallets gained exactly the fee). For PayFi Meet trades (fee to fee_to when present, else platform_wallet): escrow must also have signed; escrow lost exactly "in" of "asset"; buyer_wallet gained exactly "buyer", platform_wallet exactly "fee", seller_wallet exactly "seller_back"; no other wallet moved. A warning if the ledger time and the block time differ by more than a day. g. Nothing hidden: getSignaturesForAddress(platform_wallet) lists every transaction that mentions PayFi's wallet. Each successful one not in the ledger is fetched. Transactions whose fee payer is NOT platform_wallet were sent by others (anyone can send to PayFi) and are counted but not judged. For the rest — paid for by PayFi, as every payout is — if someone other than PayFi lost USDT/USDC while PayFi or two or more wallets gained, it looks like an unrecorded payout and is reported (with a note when it is newer than the ledger file). Transactions from the last 15 minutes (by the verifier's clock) are skipped. At most 100,000 signatures are listed and 500 unrecognised transactions fetched; beyond that a warning is given. Online, against Bitcoin (default mempool.space and blockstream.info; any Esplora-style API with --btc): h. For each Bitcoin attestation in a proof: GET /block-height/ and GET /block//header from every configured API. The 80-byte header must hash (double SHA-256) to the block hash, the hash must meet the header's proof-of-work target, and the difficulty must be at least 10^12 (real blocks in 2026 are around 10^14) — so a lying API cannot invent a block cheaply. If two APIs answer with different blocks, nothing is trusted. One answering API is enough for a PASS (proof-of-work makes a fake costly); a mismatch is a FAIL only when every configured API answered and agreed, otherwise a warning. The proof's message at the attestation must equal the header's merkle root. Then entries 1..covers.seq existed no later than that block's time. Results: PASS, FAIL (the ledger or a claim in it is wrong), WARN (could not be checked, or needs a human look), INFO. Exit code 0 when no FAIL, 1 otherwise, 2 when the ledger could not be read. 6. OpenTimestamps Proofs are standard OpenTimestamps "detached" files for a SHA-256 file hash. The "file" is the canonical JSON { seq, prev, body } of the covered entry, so its SHA-256 is that entry's hash. To check with the official tools instead: save the canonical JSON of the entry as a file, decode "ots" to file.ots, and run `ots verify file.ots` or use https://opentimestamps.org. PayFi submits a nonce-blinded hash (hash + 16 random bytes, then SHA-256) to public calendars; the calendars never see the ledger. 7. What this does not prove (limits) - Completeness relies on every payout being paid for by platform_wallet, which is how PayFi works today. A payout signed and paid by another wallet would not be seen by check g. Check g looks at USDT/USDC only: an unrecorded SOL refund, an unrecorded refund paid from PayFi's own wallet (listed as "PayFi wallet sent ..."), or a refund with zero fee (looks like a buyer paying in) would show up only in the list of other operations. - The ledger shows money that moved, not that a product was delivered. - Up to ~15 minutes of the newest activity is not yet in ledger.json, and newest entries are not yet in Bitcoin (anchoring happens within about an hour of new entries, or once a day). - Bitcoin block times can differ from real time by up to about two hours. - The "release" entry records the fingerprint of the code PayFi says it runs; the code itself is not public, so this cannot be checked from outside. - The page at payfi.cx/ledger could be changed by whoever runs payfi.cx. Use the outbound links (Solscan, mempool.space) or run payfi-verify.js yourself, ideally against your own Solana RPC and Bitcoin node. 8. Running it yourself curl -O https://payfi.cx/ledger/payfi-verify.js node payfi-verify.js (Node.js 20.19+ or 22.12+) node payfi-verify.js ledger.json --offline node payfi-verify.js --rpc https://your-solana-rpc --btc https://your-esplora/api --json The file has no dependencies and no build step; read it before running it. 9. Revision 3: PayFi ID (member numbers) One wallet, one name, one 10-digit member number (#0000000001 is PayFi). These entries are full entries in the chain, in the order they happened: reserve_plan {height, count, range: [min, max], method: "payfi-reserve/1"} Written BEFORE Bitcoin block exists. reserve_numbers {height, block_hash, numbers} numbers = sort n in min..max by sha256("payfi-reserve/1|" + block_hash + "|" + n), take the first , list them ascending. Anyone can recompute it. member {number, wallet, name, claimed_at, proof[, message, signature]} Numbers are given in order of claim and skip only reserved numbers: number = the smallest n >= 2 that is neither reserved nor given. proof "signature" carries the wallet's signed claim; "session" = claimed while signed in with the wallet; "backfill" = name held before numbers existed. link_wallet {number, owner, wallet, action: "link"|"unlink", ...signatures} A link carries two signed messages: the owner's offer and the linked wallet's acceptance. At most 10 per number. transfer_identity {number, name, from, to, former_name, dropped_links, ...signatures} The number and name move together. Signed by the old owner (offer) and the new owner (accept). #1 never moves. Signed messages are plain text with lines "Action: ...", "Wallet: ...", "Member: #..." etc.; signatures are Ed25519 over the UTF-8 text, base64. The checker verifies every signature (WebCrypto Ed25519), recomputes the reserved numbers from the block hash and, online, fetches that Bitcoin block. 10. Revision 4: PayFi Meet record (marks and blocks) PayFi Meet has no disputes and no judge. Only the seller can release the escrow to the buyer; only the buyer can send it back to the seller. PayFi has no button that moves a trade's money. Two things are recorded publicly instead: mark {at, wallet, mark, area: "meet", trade, reason} mark "no_show" reason "seller_did_not_lock" (did not lock within 30 minutes), "identify_missing" (did not send the describe-yourself form in time) or "did_not_arrive" (did not check in at the bank in time) mark "not_released" reason "stalemate": both checked in, the seller did not release within 30 days, the escrow went back to the seller with no fee (its "meet" entry, outcome "refund", is in the ledger too) mark "not_completed" reason "buyer_did_not_pay" (remote transfer mode, section 12). Counts like a no-show. block {at, wallet, area: "meet", from, until, trades: [3 trade numbers], rule} Three no-shows of one wallet within 30 days pause it for 30 days: from = the time of the third, until = from + 30 days, at = from. The checker fails a block unless the ledger already holds the three matching no-show marks of that wallet, all within the 30 days ending at "from", and the last one at exactly "from". A mark is recorded once per wallet, trade and kind. Revision 4 also adds two optional fields to "meet": chat_fp and ident_fp, the SHA-256 of the ENCRYPTED chat and descriptions of that trade when it ended: chat_fp = SHA-256("payfi-meet-chat/1\n" + lines joined by "\n"), one line per message: id|s or b (seller/buyer)|time|ciphertext ident_fp = SHA-256("payfi-meet-ident/1\n" + seller ciphertext + "\n" + buyer ciphertext) The content itself is deleted 2 hours after the trade and is never in the ledger. Escrow rent: the escrow token account is closed in the payout and its rent goes back to the seller who opened it (never to PayFi). 11. Revision 5: buyer wallet on settlements (10 Oct 2026) New settlements may carry "buyer_wallet": the wallet that paid the order. PayFi reads it from the Solana transactions that sent USDT/USDC to the order's deposit address (the owner of the sending token account; if several wallets paid, the one that sent the most). It is public information already: the deposit address and its history are on the blockchain. A payment sent from an exchange shows the exchange's wallet, not the buyer's own. The field is optional: older settlements, and settlements whose payer could not be read within 30 minutes, do not have it. The verifier only checks that it is a well-formed address. PayFi Passport uses it to count different buyers, and does not count sales paid from the seller's own (or linked) wallets. 12. Revision 6: PayFi Meet remote transfer mode (10 Oct 2026) A remote trade is a Meet trade without meeting: the buyer pays the seller by bank transfer. It is open only to sellers with a proven public record and with collateral locked in an escrow in their own name. It is switched off unless PayFi announces otherwise; when it is off, nothing below is written. Flow (all times from the server, all money moves on Solana): 1. The buyer takes the seller's offer. The seller locks the USDT/USDC in the trade's escrow within 30 minutes, or the trade is cancelled with a "no_show" mark for the seller. 2. Only after the lock the buyer sees the seller's bank account (never in the ledger, never in Telegram). The buyer transfers and presses "I have transferred" within 60 minutes, or the escrow goes back to the seller with a "not_completed" mark for the buyer. 3. After that press the escrow NEVER moves by itself. Only the seller's signed release (to the buyer, 1% fee) or the buyer's signed return (to the seller, no fee) can move it. PayFi has no button for it. 4. 7 days after the press without either, both sides get a neutral mark: mark "stalemate" reason "transfer_unresolved" It blames nobody, does not count toward a block, and the escrow stays where it is. A seller cannot open new remote trades for 7 days after a stalemate mark (older stalemate marks do not count). Remote trades are ordinary "meet" entries with one extra field: mode "transfer" (meet entries without it are trades at a bank) Seller collateral: bond {at, wallet, area: "meet", event, asset, amount, escrow, tx} event "lock" the collateral escrow of this seller holds at least the required amount; tx is null (the balance is visible at the escrow address on Solana) event "return" the whole collateral escrow was paid back to the same wallet; tx is that Solana transaction The collateral escrow is derived from PayFi's deposit key with its own label, so its address never equals a trade escrow. It can only pay its own seller: the payout transaction is built and checked exactly like a "return to seller" of a trade. It is never used to pay a buyer, a fee or anyone else. Collateral can be withdrawn only while the seller has no remote trade that is running or stuck. Default numbers (PayFi may change them; changes take effect for new trades): seller: 90 days since the first sale as seller on Meet, 30 completed sales (one buyer counts at most twice), 15 different buyers, 29,000 USDT/USDC sold, no "not_released" mark, no "stalemate" mark in the last 7 days, at most 1 no-show in 90 days, 1,000 USDT collateral, Telegram connected; one remote trade at a time; each trade at most 10% of the proven volume and at most 1,000 USDT. Changing the bank account pauses the seller's remote mode for 7 days. The verifier checks (offline): every bond entry is well-formed; each collateral escrow is locked once and returned at most once, after its lock, to the same wallet and asset, for at least the locked amount; every "meet" entry with mode "transfer" falls inside a locked period of its seller's collateral; a "stalemate" mark has reason "transfer_unresolved". Online, each "return" is checked like a Meet payout: signed by PayFi and the collateral escrow, the escrow gave exactly the amount and the seller received exactly the amount. 13. Revision 7: auctions of reserved numbers and short names (10 Oct 2026) PayFi auctions the reserved member numbers (section 9) and names of 1 to 3 characters. 100% of each winning bid goes to PayFi. Auctions are switched off unless PayFi announces otherwise. How a bid works: - The bidder needs a PayFi name. PayFi's own wallets cannot bid. - Each bid gets its own escrow address (derived from PayFi's deposit key with the label "payfi-auction-v1", never equal to any other escrow) and a message the bidder signs with their wallet: auction, item, amount, escrow, wallet. - The bid counts once the escrow holds the amount, if it is still at least the start price, or 5% (rounded up to 0.01 USDT) above the highest bid, and was signed before the auction ended. Otherwise everything in the escrow goes back. - A bid in the last 10 minutes moves the end to 10 minutes after that bid. - An outbid bid is paid back in full (the whole escrow) to the bidder's wallet. - At the end, the highest bid wins: the bid amount goes to PayFi's fee wallet, anything above it back to the bidder. Escrow rent always goes to the bidder. Entries (kind "auction", field "event"): open {auction, item: "number"|"name", value, start, asset: "USDT", starts_at, ends_at, increment_bps, extend_ms} bid {auction, bid, bidder, amount, escrow, signed_at, ends_at, message, signature} ends_at = the end after this bid refund {auction, bid, bidder, escrow, in, reason, tx} reason "outbid", or "rejected" (too low, too late, or the auction was cancelled); a refund may also return money sent late to an old bid escrow win {auction, bid, bidder, amount, escrow, in, fee, back, fee_to?, tx} fee = amount, in = fee + back close {auction, result: "no_bids"|"cancelled"} The item then moves in PayFi ID (identity entries, in order with section 9): auction_number {number, wallet, name, old_number, auction} The winner gets the reserved number. If they already had a number, it is RETIRED: never given to anyone again (the next-number rule of section 9 skips retired numbers). Name, linked wallets and history move to the new number. auction_name {number, wallet, name, old_name, auction} The short name replaces the member's name. The old name is held for its owner for 90 days, so nobody can take it to impersonate them. (A winner without a number yet simply carries the new name in their later "member" entry.) The verifier checks (offline): every bid is signed by its bidder and its signed message names the same auction, item, amount, escrow and wallet; bids rise by at least the step; the end time after each bid follows the 10-minute rule; bids were signed before the end; each win is the highest bid, settled after the end, with PayFi receiving exactly that bid; every bid that did not win is refunded (allowing one hour for a refund in flight); a number or name is given out only to the paid winner of that item, once; reserved and retired numbers follow the rules above. Online, each refund and win is checked against its Solana transaction like a Meet payout, signed by PayFi and the bid escrow. 14. Revision 8: physical goods (10 Oct 2026; rules amended the same day, before any ship entry existed) Sellers list physical products (text only; PayFi stores no photos or videos, only links to other sites chosen by the seller). Sellers and buyers may be in any country except those PayFi does not serve. A cart holds products of ONE seller: one order, one parcel, one escrow (derived with the label "payfi-ship-v1", in USDT or USDC; prices are set in USDT, 1 USDC = 1 USDT). Switched off unless PayFi announces otherwise. Every purchase is final: no chargebacks, and PayFi does not mediate. The rules are mechanical: - The price includes shipping, one price for every destination the product ships to (the seller's own country, or worldwide). The buyer pays the price into the order escrow and nothing else. "shipping" in the entry is "0". - The seller ships with a courier of their choice, pays the courier, and enters the tracking number. PayFi follows that number with a tracking service. A number already used by another order is refused. - When the courier reports the parcel delivered, the escrow pays out: PayFi gets fee = price x fee_bps / 10000 (rounded down), the seller gets price - fee, anything paid above the price goes back to the buyer. fee_bps is fixed when the order is created (default 100 = 1%, at most 1000 = 10%) and is written in the entry, so a later change of PayFi's fee never touches an order already placed. Products cost at least PayFi's minimum price. - Everything goes back to the buyer if the order is not paid in full in time, no tracking number is entered within the seller's handling time (at most 3 days), the parcel is not delivered within 21 days of shipping (45 days between countries), the parcel comes back, or the tracking number does not belong to the order (reason "tracking_invalid": the courier reports another destination country, the number was in use more than a day before the order was paid, or the courier still does not know it 4 days after it was entered). The seller has already paid the courier and bears that cost. The seller may also give the money back, with their wallet signature, any time before delivery. Entries: ship {order, outcome: "release"|"refund", reason, asset, price, shipping, in, seller, fee, buyer_back, seller_wallet, buyer_wallet, escrow, fee_bps, fee_to?, tracking_fp?, delivered_at?, tx} order = SHA-256("payfi-ledger/1|ship|" + SHA-256(order code)): only the holder of the order link can connect it to an order. tracking_fp = SHA-256("payfi-ship-tracking/1|" + salt + "|" + tracking number), with a random salt per order: whoever holds the receipt can prove it, the public cannot track the parcel. release: reason "delivered"; refund reason one of not_shipped, parcel_returned, parcel_failed, not_delivered_in_time, refunded_by_seller, not_paid_in_full, late_payment, tracking_invalid. ship_review {order, product, rating 1..5, text_sha256, buyer_wallet, seller_wallet} Only the buyer of a delivered order, once per product. The text is shown on the product page; the ledger keeps its fingerprint SHA-256("payfi-review/1|" + text). Never in the ledger: names, addresses, phone numbers, tracking numbers, product names. The seller, who ships the parcel, sees the delivery address and phone number while the order is open; afterwards only the shortened name and city. The tracking service receives only the tracking number, the courier name and the destination country. The verifier checks (offline): every order adds up as above; each order escrow is emptied once; a delivered order carries a tracking fingerprint; every review follows a delivered order of the same buyer and seller, once per product. Online, each payout is checked against its Solana transaction like a Meet payout, signed by PayFi and the order escrow.