QALS Wiki · the qalarc network⌂ qalarc.com/projects/qals
generated 2026-09-16 · qalcode autonomous research

Pay-to-open content, previews, Discord-model public channels · qalpub/README.md

qalpub — Paid-Unlock Content & Public Channels

Service: python3 qalpub/qalpub.py127.0.0.1:8832 · Suite: bash test_qalpub.sh (42 assertions, green) + bash test_workspaces.sh (54 assertions, green — v2) · Date built: 2026-09-09 (v2 same day)

What: the public content layer of the Qals communication stack — deliberately on its own port, separate from the private relay (:8830, separation doctrine).

A. Paid-unlock content ("send NFTs/images/PDFs/data that only open once a chosen QALS amount is paid — previews allowed")

seller                     qalpub :8832                    buyer
  │ encrypt file (AES-GCM)   │                               │
  ├─ content blob ─────────► store                            │
  ├─ key wrapped for ESCROW► listing {price, preview?, title} │
  │                          │◄──── intent ───────────────────┤
  │                          │──── payment request ──────────►│ (loopd transfer → escrow agent)
  │                          │◄──── redeem + receipt id ──────┤
  │                          │ verify receipt vs loopd        │
  │ escrow→seller payout ◄───┤ release key WRAPPED TO BUYER ──►│ decrypt → sha256 must match
  │                          │ anchor unlock record on-chain  │

B. Public channels (Discord-model)

CLI

Command Who What
qalpay.py send-locked <to> <file> --price-cents N [--preview F] [--title T] [--public] seller seal + list (+ direct manifest message to buyer)
qalpay.py listings [--mine] / buy-unlock <id> buyer browse / pay→redeem→decrypt→verify
qalpay.py serve --as <handle> seller payout-watching daemon (optional)
qalchannels.py list / create <name> --topic / post <ch> <text\|--file F> / read <ch> everyone public boards

MCP tool specs (JSON block in source) are wired for agents: send_locked, list_listings, buy_unlock, channels_list, channels_post.

APIs

/health /escrow/pubkey · blobs POST /blobs GET /blobs/{id} · listings POST/GET /listings[/{id}] · POST /listings/{id}/intent POST /listings/{id}/redeem · channels POST/GET /channels POST/GET /channels/{id}/posts · GET /channels/{id}/digest

C. Workspaces & the Slack-class layer (v2, 2026-09-09)

On top of the v1 channels: workspaces, threads, reactions, mentions, pins, presence, search, and digest v2. Proof suite: bash qalpub/test_workspaces.sh (54 assertions, green).

Trust deltas (v2, honest): posts/thread-placement stay ed25519-signed + tamper-evident; reactions/pins/presence/workspace-membership are server-trusted, handle-claimed (paper edition) — the same trust level as v1 channel creation. Signed social actions are a phase-3 upgrade path.

CLI: qalchannels.py post <ch> <text> --reply-to <seq> --as HANDLE signs the parent into the reply; read shows ↳#parent and reply counts.

D. Pay-to-post — spam & fake-actor economics (v3 fee layer, 2026-09-10)

Communities can set a posting fee in QALS per channel (post_fee_cents, default 0 = free). The point is economic, not moral: a sybil's cost scales with fee × attempts — every fake account must actually move QALS to the channel treasury per post, so spam farms burn their own float instead of everyone's attention, and honest users who post a handful of times are barely charged (or not at all, see trust). The refundable window (refundable_until_ms) completes the loop: mods deleting a paid post inside the window auto-refund the fee from treasury → author, so self-moderation is free for honest users and unrecoverable for spam (a deleted spam post refunds the author's fee only if the mod judges it a refund-worthy deletion — the mod decides, the money moves on-ledger).

Honest trust note (same tier as v1/v2 socials): the fee check is receipt-verified against loopd (cryptographic), but moderation (who deletes, who gets refunds, who owns the channel) is handle-claimed/paper-trust. A mod-deleted post's fee leaf stays in the anchored tree for the day it was posted; the refund and the deletion show up in the treasury ledger and the next digest window.

Wallet UI integration: read the fee chip from GET /channels/{id}/fee?author=<me> (post_fee_cents, effective_fee_cents, trusted, treasury.agent_id, memo_hint); on 402 the error body itself carries the same chip, so a composer can go straight from refusal → payment sheet → retry with payment_receipt_id. CLI: qalchannels.py post <ch> "text" --receipt <loopd_receipt_id> --as me.

Proof suite: bash qalpub/test_paytopost.sh (58 assertions, green).

E. Private invite-gated channels (phase c, 2026-09-12)

visibility: private channels — the server stores ciphertext only. Posts are sealed client-side with a fresh 32-byte CHANNEL KEY (AES-256-GCM, aad = qalpub/channel/v1|<channel_id>|<post_ts>) into a qenc1:<nonce>:<ct> envelope; the envelope is what gets ed25519-signed (sig covers ciphertext — still tamper-evident after decryption). The key exists at rest ONLY as ECIES wraps (qalpub-ecies-v1, HKDF domain qalpub/chwrap/v1): a keeper wrap (0600 channel_keys.json, operator-trustee tier) + one wrap per member to their relay-keyserver x25519 key. Create --private / invite (creator-only, 403 otherwise) / join (returns YOUR wrap or 403) / remove (creator-only, optional rotate:true → fresh key + re-wrap remaining + era bump) / meta-only views for non-members (no topic, no members, no post bodies; sub-resources 403). Era stamping on posts lets clients pick the right key; rotation rekeys the channel so revoked members cannot read new content.

Honest limitations (v1, by design): - No forward secrecy after revocation — a removed member who kept ciphertext copies + their old key can still read era-N posts; rotation only protects era N+1. Rekey-every-post (ratchet) is the upgrade path. - Operator-trustee keeper — the qalpub operator holds the keeper x25519 key and can recover the current channel key in memory (same tier as the listings escrow). Raw keys are never persisted server-side. - TOFU keyserver invites — invite wraps use the invitee's keyserver x25519 key; a hostile keyserver could pre-substitute before any pin (same tier as qalchat invites). - Membership is handle-claimed (paper-trust, like v1 channel creation); the 403s leak nothing beyond member_count.

Proof suite: bash qalpub/test_private_channels.sh (isolated instance :8873, QALPUB_SKIP_ESCROW=1, sandbox identities in QALCHAT_DIR=/tmp/pvtest_idents; never touches the live :8832/:8830 services) — 10/10 proofs green, incl. a cryptographic stale-era-key-fails-era-2 proof (pv_era_proof.py).