Skip to main content

Cast

Consensus is not uniformity — it is aligned energy.

One relevance vote. Three weights: ±1, ±3, and ±10. Your lore rank decides how heavy your vote can be. Casting spends energy from your monthly pool. Receiving votes on your content earns energy back and moves your rank.

A topic vote leaves branch_id empty. A branch vote sets it. Stored weights are ±1, ±3, and ±10. The voter's lore rank caps the weight. Poll and proposal options accept only +1.

Rank, Received, and the tier table: Rank. Energy cost and the monthly pool: Energy. Home order: Feed. Branches: Branches.

How it works​

Casting requires a signed-in session (X, GitHub, or Google). Each cast spends the vote:cast budget, 10 per 10 seconds, and the shared per-IP write budget.

flowchart TD
vote["Vote ±1 · ±3 · ±10"] --> cap["Rank cap"]
cap --> gate["Own content or closed topic"]
gate --> row["One votes row"]
row --> topic["Topic score ± delta"]
row --> branch["Branch score, when a branch is selected"]
topic --> received["Author Received"]
row --> first["First insert"]
first --> plus["+1 energy to the topic author"]
first --> hot["+130 energy if the live count hits 100 within 48h"]

The vote​

PropertyRule
TargetTopic vote (branch_id null) and any number of branch votes on that topic. One row per target.
Weights±1, ±3, ±10. The column check allows all six. The rank cap is applied in votes.cast.
CapNewbie ±1, Geek ±3, Wizard and GOAT ±10. The cap is the voter's Received net. Tiers: Rank.
ChangeMove the weight or flip the sign. The six stored scores have no zero. The same score again does nothing: no spend, no score change.
Own contentOwn topic, or a branch you proposed, returns FORBIDDEN (Cannot vote on your own content).
Closedstatus other than open, or expires_at already passed, returns FORBIDDEN (Closed topics do not accept votes).
BranchThe branch must belong to the topic.
Energyabs(next) − abs(previous). A lower weight refunds. +3 → −3 costs 0 and still stamps lastActiveAt. Details: Energy.

The vote control shows 1, 3, and 10 for every rank. A weight above the signed-in cap stays visible and inactive. The server still rejects it (This rank can cast up to ±N). A stored weight that is now above a later, lower cap stays until that user changes it.

What a cast writes​

Delta on a change is next − previous. On a first insert the delta is the score.

ColumnUpdate
topics.relevance_score+ delta for a topic vote and for a branch vote
branches.relevance_score+ delta only when a branch is selected

The topic score is the signed sum of every vote on that topic, including topic votes. A sum of branch scores alone drops the topic votes. Empty topic = 0. Either score can be negative. Updates are column + delta.

Public JSON key vote is topics.relevance_score. The same number is relevance_score on topic payloads. Overlay maps (current_user_relevance_vote, current_user_branch_votes) stay on the signed-in response. They are not a public file.

Received is that same signed sum on the author's work. A vote change revises it. The public plate is {tier} {score}. Formula: Rank.

A first insert grants the topic author +1 energy (vote_received) and checks the hot bonus. A later change of the same row does neither. Creating a topic adds 0 Received. Hot +130 is energy: Energy.

votes.created_at is the insert time. A normal weight change leaves it. An exclusive option switch restamps the surviving row so the newest option is the one shown.

Poll and proposal​

Options are exclusive +1, one option per user. Any other score returns BAD_REQUEST (This topic only accepts +1). The rank cap is not consulted, because +1 is legal at every tier.

Choosing the option you already hold is a no-op. Switching option deletes the extra rows, moves the surviving row onto the new branch, and sets its score to 1. Topic and branch scores move by the signed delta. Energy cost is abs(1) − abs(sum of that user's option rows). One +1 moving to another option costs 0.

A topic vote on these types is a separate row. It still uses the rank cap and the six weights.

Table votes​

Live schema: packages/shared/src/schema/votes.ts. Applied in 0004_per_branch_relevance.sql and 0005_relevance_score_only.sql.

ColumnTypeNullMeaning
idUUIDnoPrimary key
user_idUUIDnoVoter. Cascade on user delete
topic_idUUIDnoTopic. Cascade on topic delete
branch_idUUIDyesNull = topic vote. Cascade on branch delete
scoreintegernoOne of 1, −1, 3, −3, 10, −10
created_attimestamptznoInsert time
CREATE UNIQUE INDEX votes_user_branch_idx ON votes (user_id, branch_id) WHERE branch_id IS NOT NULL;
CREATE UNIQUE INDEX votes_user_topic_idx ON votes (user_id, topic_id) WHERE branch_id IS NULL;
CHECK (score IN (1, -1, 3, -3, 10, -10))

Also indexed: user_id, (user_id, created_at DESC), branch_id, topic_id.

vote_type is gone. topics.vote_count is gone. branches.vote_count was renamed to relevance_score.

Home index: (relevance_score DESC, created_at DESC) where status = 'open'. List rules: Feed.

If a stored score drifts​

UPDATE branches b
SET relevance_score = COALESCE((
SELECT SUM(v.score) FROM votes v WHERE v.branch_id = b.id
), 0);

UPDATE topics t
SET relevance_score = COALESCE((
SELECT SUM(v.score) FROM votes v WHERE v.topic_id = t.id
), 0);

The topic statement sums every vote on the topic. Summing branches.relevance_score leaves topic votes out of Home.

Code​

PiecePath
Schemapackages/shared/src/schema/votes.ts
Inputpackages/shared/src/validators/votes.ts (castVoteInputSchema)
Castapps/frontend/src/server/routers/votes.ts
Vote controlapps/frontend/src/components/topic/RelevanceVote.tsx
Clientapps/frontend/src/hooks/use-cast-vote.ts