Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve rewards #1731

Merged
merged 12 commits into from
Dec 18, 2024
2 changes: 1 addition & 1 deletion api/resolvers/rewards.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default {
const [{ to, from }] = await models.$queryRaw`
SELECT date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago' as from,
(date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') + interval '1 day - 1 second' as to`
return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 100 }, { models, ...context })
return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 500 }, { models, ...context })
ekzyis marked this conversation as resolved.
Show resolved Hide resolved
},
total: async (parent, args, { models }) => {
if (!parent.total) {
Expand Down
2 changes: 1 addition & 1 deletion api/resolvers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export async function topUsers (parent, { cursor, when, by, from, to, limit = LI
}

const users = (await models.$queryRawUnsafe(`
SELECT *
SELECT *, ${column === 'proportion' ? 'proportion' : ''}
huumn marked this conversation as resolved.
Show resolved Hide resolved
FROM
(SELECT users.*,
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
Expand Down
2 changes: 1 addition & 1 deletion api/typeDefs/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default gql`
bioId: Int
photoId: Int
since: Int

proportion: Float
huumn marked this conversation as resolved.
Show resolved Hide resolved
optional: UserOptional!
privates: UserPrivates

Expand Down
8 changes: 4 additions & 4 deletions components/user-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ export function User ({ user, rank, statComps, className = 'mb-2', Embellish, ny
<div className={styles.other}>
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
</div>}
{Embellish && <Embellish rank={rank} />}
{Embellish && <Embellish rank={rank} user={user} />}
</UserBase>
</>
)
}

function UserHidden ({ rank, Embellish }) {
function UserHidden ({ rank, user, Embellish }) {
return (
<>
{rank
Expand All @@ -133,7 +133,7 @@ function UserHidden ({ rank, Embellish }) {
<div className={`${styles.title} text-muted d-inline-flex align-items-center`}>
stacker is in hiding
</div>
{Embellish && <Embellish rank={rank} />}
{Embellish && <Embellish rank={rank} user={user} />}
</div>
</div>
</>
Expand All @@ -148,7 +148,7 @@ export function ListUsers ({ users, rank, statComps = DEFAULT_STAT_COMPONENTS, E
{users.map((user, i) => (
user
? <User key={user.id} user={user} rank={rank && i + 1} statComps={statComps} Embellish={Embellish} nymActionDropdown={nymActionDropdown} />
: <UserHidden key={i} rank={rank && i + 1} Embellish={Embellish} />
: <UserHidden key={i} rank={rank && i + 1} user={user} Embellish={Embellish} />
))}
</div>
)
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,6 @@ services:
- bitcoin
- sn_lnd
- lnd
- cln
restart: unless-stopped
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
volumes:
Expand Down
2 changes: 1 addition & 1 deletion fragments/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export const TOP_USERS = gql`
photoId
ncomments(when: $when, from: $from, to: $to)
nposts(when: $when, from: $from, to: $to)

proportion
optional {
stacked(when: $when, from: $from, to: $to)
spent(when: $when, from: $from, to: $to)
Expand Down
9 changes: 5 additions & 4 deletions pages/rewards/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { useToast } from '@/components/toast'
import { useLightning } from '@/components/lightning'
import { ListUsers } from '@/components/user-list'
import { Col, Row } from 'react-bootstrap'
import { proportions } from '@/lib/madness'
import { useData } from '@/components/use-data'
import { GrowthPieChartSkeleton } from '@/components/charts-skeletons'
import { useMemo } from 'react'
Expand Down Expand Up @@ -50,6 +49,7 @@ ${ITEM_FULL_FIELDS}
photoId
ncomments
nposts
proportion

optional {
streak
Expand Down Expand Up @@ -117,9 +117,10 @@ export default function Rewards ({ ssrData }) {

if (!dat) return <PageLoading />

function EstimatedReward ({ rank }) {
const referrerReward = Math.floor(total * proportions[rank - 1] * 0.2)
const reward = Math.floor(total * proportions[rank - 1]) - referrerReward
function EstimatedReward ({ rank, user }) {
if (!user) return null
const referrerReward = Math.max(Math.floor(total * user.proportion * 0.2), 0)
const reward = Math.max(Math.floor(total * user.proportion) - referrerReward, 0)

return (
<div className='text-muted fst-italic'>
Expand Down
90 changes: 90 additions & 0 deletions prisma/migrations/20241217163642_user_values_improve/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
CREATE OR REPLACE FUNCTION user_values(
min TIMESTAMP(3), max TIMESTAMP(3), ival INTERVAL, date_part TEXT,
percentile_cutoff INTEGER DEFAULT 50,
each_upvote_portion FLOAT DEFAULT 4.0,
each_item_portion FLOAT DEFAULT 4.0,
handicap_ids INTEGER[] DEFAULT '{616, 6030, 4502, 27}',
handicap_zap_mult FLOAT DEFAULT 0.3)
RETURNS TABLE (
t TIMESTAMP(3), id INTEGER, proportion FLOAT
)
LANGUAGE plpgsql
AS $$
DECLARE
min_utc TIMESTAMP(3) := timezone('utc', min AT TIME ZONE 'America/Chicago');
BEGIN
RETURN QUERY
SELECT period.t, u."userId", u.total_proportion
FROM generate_series(min, max, ival) period(t),
LATERAL
(WITH item_ratios AS (
SELECT *,
CASE WHEN "parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type,
CASE WHEN "weightedVotes" > 0 THEN "weightedVotes"/(sum("weightedVotes") OVER (PARTITION BY "parentId" IS NULL)) ELSE 0 END AS ratio
FROM (
SELECT *,
NTILE(100) OVER (PARTITION BY "parentId" IS NULL ORDER BY ("weightedVotes"-"weightedDownVotes") desc) AS percentile,
ROW_NUMBER() OVER (PARTITION BY "parentId" IS NULL ORDER BY ("weightedVotes"-"weightedDownVotes") desc) AS rank
FROM
"Item"
WHERE date_trunc(date_part, created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t
AND "weightedVotes" > 0
AND "deletedAt" IS NULL
AND NOT bio
AND ("invoiceActionState" IS NULL OR "invoiceActionState" = 'PAID')
) x
WHERE x.percentile <= percentile_cutoff
),
-- get top upvoters of top posts and comments
upvoter_islands AS (
SELECT "ItemAct"."userId", item_ratios.id, item_ratios.ratio, item_ratios."parentId",
"ItemAct".msats as tipped, "ItemAct".created_at as acted_at,
ROW_NUMBER() OVER (partition by item_ratios.id order by "ItemAct".created_at asc)
- ROW_NUMBER() OVER (partition by item_ratios.id, "ItemAct"."userId" order by "ItemAct".created_at asc) AS island
FROM item_ratios
JOIN "ItemAct" on "ItemAct"."itemId" = item_ratios.id
WHERE act = 'TIP'
AND date_trunc(date_part, "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID')
),
-- isolate contiguous upzaps from the same user on the same item so that when we take the log
-- of the upzaps it accounts for successive zaps and does not disproportionately reward them
-- quad root of the total tipped
upvoters AS (
SELECT "userId", upvoter_islands.id, ratio, "parentId", GREATEST(power(sum(tipped) / 1000, 0.25), 0) as tipped, min(acted_at) as acted_at
FROM upvoter_islands
GROUP BY "userId", upvoter_islands.id, ratio, "parentId", island
),
-- the relative contribution of each upvoter to the post/comment
-- early component: 1/ln(early_rank + e - 1)
-- tipped component: how much they tipped relative to the total tipped for the item
-- multiplied by the relative rank of the item to the total items
-- multiplied by the trust of the user
upvoter_ratios AS (
SELECT "userId", sum((early_multiplier+tipped_ratio)*ratio*CASE WHEN users.id = ANY (handicap_ids) THEN handicap_zap_mult ELSE users.trust+0.1 END) as upvoter_ratio,
"parentId" IS NULL as "isPost", CASE WHEN "parentId" IS NULL THEN 'TIP_POST' ELSE 'TIP_COMMENT' END as type
FROM (
SELECT *,
1.0/LN(ROW_NUMBER() OVER (partition by upvoters.id order by acted_at asc) + EXP(1.0) - 1) AS early_multiplier,
tipped::float/(sum(tipped) OVER (partition by upvoters.id)) tipped_ratio
FROM upvoters
WHERE tipped > 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this changed from > 0 to > 2?

Copy link
Member Author

@huumn huumn Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enforces a minimum zap to be considered for zapping rewards. Because we use the quad root of the zap amount, this means the minimum zap for consideration is 2 = x^0.25 or x = 16 sats.

It's arbitrary.

I thought about increasing it more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should increase it more.

Copy link
Member Author

@huumn huumn Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative to setting this limit would to weight zap order by the zap amount.

Rather than 1/ln(zap_number) + zap_size_relative, do zap_size_relative*(1/ln(zap_number) + 1).

Conceptually this would mean that by being first, all things being equal, you could double the "value" of your zap. And if your zap is really small relative to other zaps, doubling its "value" wouldn't amount to much.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should increase it more.

Sounds good, I would rather make it too large than too small

An alternative to setting this limit would to weight zap order by the zap amount.

This would mean that zap amount now becomes more important than zap order, no?

) u
JOIN users on "userId" = users.id
GROUP BY "userId", "parentId" IS NULL
),
proportions AS (
SELECT "userId", NULL as id, type, ROW_NUMBER() OVER (PARTITION BY "isPost" ORDER BY upvoter_ratio DESC) as rank,
upvoter_ratio/(sum(upvoter_ratio) OVER (PARTITION BY "isPost"))/each_upvote_portion as proportion
FROM upvoter_ratios
WHERE upvoter_ratio > 0
UNION ALL
SELECT "userId", item_ratios.id, type, rank, ratio/each_item_portion as proportion
FROM item_ratios
)
SELECT "userId", sum(proportions.proportion) AS total_proportion
FROM proportions
GROUP BY "userId"
HAVING sum(proportions.proportion) > 0.000001) u;
END;
$$;
20 changes: 10 additions & 10 deletions worker/earn.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { notifyEarner } from '@/lib/webPush'
import createPrisma from '@/lib/create-prisma'
import { proportions } from '@/lib/madness'
import { SN_NO_REWARDS_IDS } from '@/lib/constants'

const TOTAL_UPPER_BOUND_MSATS = 1_000_000_000
Expand Down Expand Up @@ -40,18 +39,19 @@ export async function earn ({ name }) {

/*
How earnings (used to) work:
1/3: top 21% posts over last 36 hours, scored on a relative basis
1/3: top 21% comments over last 36 hours, scored on a relative basis
1/3: top 50% posts over last 36 hours, scored on a relative basis
1/3: top 50% comments over last 36 hours, scored on a relative basis
1/3: top upvoters of top posts/comments, scored on:
- their trust
- how much they tipped
- how early they upvoted it
- how the post/comment scored

Now: 80% of earnings go to top 100 stackers by value, and 10% each to their forever and one day referrers
Now: 80% of earnings go to top stackers by relative value, and 10% each to their forever and one day referrers
*/

// get earners { userId, id, type, rank, proportion, foreverReferrerId, oneDayReferrerId }
// has to earn at least 125000 msats to be eligible (so that they get at least 1 sat after referrals)
const earners = await models.$queryRaw`
WITH earners AS (
SELECT users.id AS "userId", users."referrerId" AS "foreverReferrerId",
Expand All @@ -63,8 +63,8 @@ export async function earn ({ name }) {
'day') uv
JOIN users ON users.id = uv.id
WHERE NOT (users.id = ANY (${SN_NO_REWARDS_IDS}))
AND uv.proportion >= 0.0000125
ekzyis marked this conversation as resolved.
Show resolved Hide resolved
ORDER BY proportion DESC
LIMIT 100
)
SELECT earners.*,
COALESCE(
Expand All @@ -86,10 +86,10 @@ export async function earn ({ name }) {
let total = 0

const notifications = {}
for (const [i, earner] of earners.entries()) {
for (const [, earner] of earners.entries()) {
const foreverReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
let oneDayReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
const earnerEarnings = Math.floor(parseFloat(proportions[i] * sum)) - foreverReferrerEarnings - oneDayReferrerEarnings
const earnerEarnings = Math.floor(parseFloat(earner.proportion * sum)) - foreverReferrerEarnings - oneDayReferrerEarnings

total += earnerEarnings + foreverReferrerEarnings + oneDayReferrerEarnings
if (total > sum) {
Expand All @@ -108,7 +108,7 @@ export async function earn ({ name }) {
'oneDayReferrer', earner.oneDayReferrerId,
'oneDayReferrerEarnings', oneDayReferrerEarnings)

if (earnerEarnings > 0) {
if (earnerEarnings > 1000) {
stmts.push(...earnStmts({
msats: earnerEarnings,
userId: earner.userId,
Expand Down Expand Up @@ -140,7 +140,7 @@ export async function earn ({ name }) {
}
}

if (earner.foreverReferrerId && foreverReferrerEarnings > 0) {
if (earner.foreverReferrerId && foreverReferrerEarnings > 1000) {
stmts.push(...earnStmts({
msats: foreverReferrerEarnings,
userId: earner.foreverReferrerId,
Expand All @@ -153,7 +153,7 @@ export async function earn ({ name }) {
oneDayReferrerEarnings += foreverReferrerEarnings
}

if (earner.oneDayReferrerId && oneDayReferrerEarnings > 0) {
if (earner.oneDayReferrerId && oneDayReferrerEarnings > 1000) {
stmts.push(...earnStmts({
msats: oneDayReferrerEarnings,
userId: earner.oneDayReferrerId,
Expand Down
Loading