diff --git a/api/resolvers/rewards.js b/api/resolvers/rewards.js
index 3dcfd2c2..a65ebc43 100644
--- a/api/resolvers/rewards.js
+++ b/api/resolvers/rewards.js
@@ -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 })
},
total: async (parent, args, { models }) => {
if (!parent.total) {
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index e836b22b..03f6a8d1 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -66,11 +66,12 @@ export async function topUsers (parent, { cursor, when, by, from, to, limit = LI
case 'comments': column = 'ncomments'; break
case 'referrals': column = 'referrals'; break
case 'stacking': column = 'stacked'; break
+ case 'value':
default: column = 'proportion'; break
}
const users = (await models.$queryRawUnsafe(`
- SELECT *
+ SELECT * ${column === 'proportion' ? ', proportion' : ''}
FROM
(SELECT users.*,
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 8b100170..e61cb4b7 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -59,6 +59,11 @@ export default gql`
photoId: Int
since: Int
+ """
+ this is only returned when we sort stackers by value
+ """
+ proportion: Float
+
optional: UserOptional!
privates: UserPrivates
diff --git a/components/user-list.js b/components/user-list.js
index 40f77286..90be34f8 100644
--- a/components/user-list.js
+++ b/components/user-list.js
@@ -107,13 +107,13 @@ export function User ({ user, rank, statComps, className = 'mb-2', Embellish, ny
{statComps.map((Comp, i) => )}
}
- {Embellish && }
+ {Embellish && }
>
)
}
-function UserHidden ({ rank, Embellish }) {
+function UserHidden ({ rank, user, Embellish }) {
return (
<>
{rank
@@ -133,7 +133,7 @@ function UserHidden ({ rank, Embellish }) {
stacker is in hiding
- {Embellish && }
+ {Embellish && }
>
@@ -148,7 +148,7 @@ export function ListUsers ({ users, rank, statComps = DEFAULT_STAT_COMPONENTS, E
{users.map((user, i) => (
user
?
- :
+ :
))}
)
diff --git a/docker-compose.yml b/docker-compose.yml
index ca015448..40406609 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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:
diff --git a/fragments/users.js b/fragments/users.js
index da207861..b96ec932 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -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)
diff --git a/pages/rewards/index.js b/pages/rewards/index.js
index 164710f4..a81d8843 100644
--- a/pages/rewards/index.js
+++ b/pages/rewards/index.js
@@ -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'
@@ -50,6 +49,7 @@ ${ITEM_FULL_FIELDS}
photoId
ncomments
nposts
+ proportion
optional {
streak
@@ -117,9 +117,10 @@ export default function Rewards ({ ssrData }) {
if (!dat) return
- 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 (
diff --git a/prisma/migrations/20241217163642_user_values_improve/migration.sql b/prisma/migrations/20241217163642_user_values_improve/migration.sql
new file mode 100644
index 00000000..47f21be3
--- /dev/null
+++ b/prisma/migrations/20241217163642_user_values_improve/migration.sql
@@ -0,0 +1,93 @@
+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.1
+ ) 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;
+$$;
+
+REFRESH MATERIALIZED VIEW CONCURRENTLY user_values_today;
+REFRESH MATERIALIZED VIEW CONCURRENTLY user_values_days;
\ No newline at end of file
diff --git a/worker/earn.js b/worker/earn.js
index 63034259..019cf9bb 100644
--- a/worker/earn.js
+++ b/worker/earn.js
@@ -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
@@ -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",
@@ -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
ORDER BY proportion DESC
- LIMIT 100
)
SELECT earners.*,
COALESCE(
@@ -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) {
@@ -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,
@@ -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,
@@ -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,