Fix account_too_new false positive for old Nostr accounts

- Force refresh profile when denying account_too_new, then re-check
- Use querySync for age probe instead of pool.get (all relays, more reliable)
- Add default relays: nostr.info, snort.social, mostr.pub
- Skip cache when cached first-seen is borderline (10-14 days)

Made-with: Cursor
This commit is contained in:
Michilis
2026-03-16 18:16:05 +00:00
parent a1509f21fc
commit a47868d17c
3 changed files with 24 additions and 14 deletions

View File

@@ -63,8 +63,8 @@ export const config = {
ipCooldownDays: envInt("IP_COOLDOWN_DAYS", 7),
maxClaimsPerIpPerPeriod: envInt("MAX_CLAIMS_PER_IP_PER_PERIOD", 1),
// Nostr (defaults include relays common for remote signers / NIP-05)
nostrRelays: (process.env.NOSTR_RELAYS ?? "wss://relay.damus.io,wss://relay.nostr.band,wss://relay.getalby.com,wss://nos.lol").split(",").map((s) => s.trim()),
// Nostr (defaults include relays common for remote signers, snort, iris, and NIP-05)
nostrRelays: (process.env.NOSTR_RELAYS ?? "wss://relay.damus.io,wss://relay.nostr.band,wss://relay.getalby.com,wss://nos.lol,wss://relay.nostr.info,wss://relay.snort.social,wss://relay.mostr.pub").split(",").map((s) => s.trim()),
relayTimeoutMs: envInt("RELAY_TIMEOUT_MS", 5000),
maxEventsFetch: envInt("MAX_EVENTS_FETCH", 500),
metadataCacheHours: envInt("METADATA_CACHE_HOURS", 24),

View File

@@ -93,10 +93,14 @@ export async function checkEligibility(pubkey: string, ipHash: string): Promise<
};
}
const profile = await fetchAndScorePubkey(pubkey);
let profile = await fetchAndScorePubkey(pubkey);
const minAgeSec = config.minAccountAgeDays * SECONDS_PER_DAY;
const cutoff = now - minAgeSec;
console.log(`[eligibility] pubkey=${pubkey.slice(0, 8)}… firstSeen=${profile.nostrFirstSeenAt} cutoff=${cutoff} score=${profile.activityScore}`);
if (profile.nostrFirstSeenAt === null || profile.nostrFirstSeenAt > cutoff) {
profile = await fetchAndScorePubkey(pubkey, true);
console.log(`[eligibility] force-refresh after account_too_new: firstSeen=${profile.nostrFirstSeenAt}`);
}
if (profile.nostrFirstSeenAt === null || profile.nostrFirstSeenAt > cutoff) {
const ageDays = profile.nostrFirstSeenAt ? Math.floor((now - profile.nostrFirstSeenAt) / SECONDS_PER_DAY) : "null";
console.log(`[eligibility] DENIED account_too_new: firstSeen age=${ageDays} days, required=${config.minAccountAgeDays}`);

View File

@@ -20,21 +20,23 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
}
/**
* pool.get resolves as soon as ONE relay responds (or null if none do).
* Much faster than querySync which waits for EOSE from ALL relays.
* Query all relays for old events (created_at <= untilTimestamp). Uses querySync to wait
* for all relays and merge results, improving reliability vs pool.get (first relay only).
*/
async function probeOldEvent(pubkey: string, untilTimestamp: number): Promise<number | null> {
try {
const ev = await withTimeout(
pool.get(config.nostrRelays, {
const evs = await withTimeout(
pool.querySync(config.nostrRelays, {
kinds: [0, 1, 3],
authors: [pubkey],
until: untilTimestamp,
limit: 1,
limit: 10,
}),
config.relayTimeoutMs
);
return ev ? ev.created_at : null;
const arr = Array.isArray(evs) ? evs : [];
if (arr.length === 0) return null;
return Math.min(...arr.map((e) => e.created_at));
} catch {
return null;
}
@@ -50,8 +52,14 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
const nowSec = Math.floor(Date.now() / 1000);
const cacheHours = config.metadataCacheHours;
const cacheValidUntil = (cached?.last_metadata_fetch_at ?? 0) + cacheHours * 3600;
const ageCutoffSec = nowSec - config.minAccountAgeDays * 86400;
const borderlineWindowSec = 4 * 86400;
const isBorderline =
cached?.nostr_first_seen_at != null &&
cached.nostr_first_seen_at >= ageCutoffSec &&
cached.nostr_first_seen_at <= ageCutoffSec + borderlineWindowSec;
if (!forceRefreshProfile && cached && cacheValidUntil > nowSec) {
if (!forceRefreshProfile && cached && cacheValidUntil > nowSec && !isBorderline) {
return {
nostrFirstSeenAt: cached.nostr_first_seen_at,
notesCount: cached.notes_count,
@@ -62,16 +70,14 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
}
// Run the main activity query and the account-age probe in parallel.
// The age probe uses pool.get (resolves on first relay hit) so it's fast.
const ageCutoff = nowSec - config.minAccountAgeDays * 86400;
// The age probe uses querySync to wait for all relays and merge results for reliability.
const mainQueryPromise = withTimeout(
pool.querySync(config.nostrRelays, { kinds: [0, 1, 3], authors: [pubkey], limit: config.maxEventsFetch }),
config.relayTimeoutMs
).then((r) => (Array.isArray(r) ? r : []))
.catch((): { kind: number; created_at: number; content?: string; tags: string[][] }[] => []);
const ageProbePromise = probeOldEvent(pubkey, ageCutoff);
const ageProbePromise = probeOldEvent(pubkey, ageCutoffSec);
const [events, oldEventTimestamp] = await Promise.all([mainQueryPromise, ageProbePromise]);