POUNTY
Login

Expiry and refunds

One scheduled job moves everything through time: it activates funded dares, expires the ones that ran out, and sweeps their escrows back to whoever paid.

The scheduler

A separate worker calls a single endpoint on the app, POST /api/admin/process-expired, every CRON_INTERVAL seconds. The default is 30 seconds, and the worker gives each call 25 seconds before it gives up and tries again on the next beat. Nothing else in the system expires or refunds anything on a timer.

Each tick takes a Postgres advisory lock. A tick that starts while another is still working returns { "skipped": true } immediately instead of racing it, so a slow RPC or a second replica cannot double-process a dare. Every step inside the tick is a conditional update, which makes re-running a tick a no-op.

#StepScope
0Check unverified escrows on-chain: activate the funded ones, cancel the empty ones, schedule refunds for late and partial payments.Newest 200 per tick
1Release per-dare refund locks left behind by a process that died.Locks older than 10 minutes
2Expire acceptances whose time is up.All
3Return payouts stuck in processing to approved so an admin can retry them.Stuck for over 10 minutes
4Expire dares whose timer has run out, funded or not. Nothing is deleted.All
5Reopen a dare whose submissions were all rejected while it still has time left.All
6Refund expired, funded dares with nothing left to review.Oldest 25 per tick

Dare states

StateTriggerNext state
pending_paymentCreatedWaiting for funds. The escrow address is shown to the creator.
pending_paymentEscrow balance covers the rewardactive, with the timer starting at that moment
pending_paymentEmpty for more than 1 hour, or the creator cancelscancelled
pending_paymentPartly funded and abandoned for more than 24 hoursexpired, marked funded, then refunded
cancelledMoney arrives within 24 hours of creationexpired, marked funded, then refunded. The dare never comes back to life.
activeFirst submission arrivespending_review
pending_reviewLast live submission rejected while time remainsactive
activeTimer runs outexpired
pending_reviewTimer runs out with a submission still liveexpired, held for review, not refunded
pending_reviewAdmin approves a submissioncompleted, escrow paid to the winner
expiredA submission was still under review and the admin approves itcompleted, escrow paid to the winner
expiredNo live submissions and the escrow is fundedrefunded, escrow swept to the refund destination
any state except completed or refundedAdmin removes the darecancelled if it was never funded, otherwise expired and refunded immediately

Submission and acceptance states

StateTriggerNext state
submission pendingAdmin approvesprocessing
submission processingPayout confirmedapproved with a payment hash
submission processingPayout failed, or the worker died and 10 minutes passedapproved with no payment hash, waiting for an admin retry
submission pendingAdmin rejects, or another submission wonrejected
acceptance activeThe dare deadline passesexpired
acceptance activeThe hunter submitssubmitted
acceptance activeAdmin removes the dareabandoned
acceptance expired or abandonedThe hunter accepts again while the dare is still liveactive with a refreshed deadline

A submission in pending, processing or approved is live, and while one exists the escrow is reserved for a hunter and is never refunded. That is why an expired dare with a submission still under review sits in expired until an admin decides it.

Where a refund goes

The destination is resolved in a fixed order, and only a well-formed EVM address counts at each step.

OrderDestinationNotes
1The refund address on the dareWhat the creator typed when posting. Required for ETH rewards.
2The creator's linked walletThe wallet on the account, if there is one and it parses as an address.
3The recorded payerThe address the server already identified as having funded the escrow.
4The payer found on-chainThe sender of the recorded funding transaction, or the first ERC-20 transfer into the escrow, read from the logs and then stored.

Step 4 works for tokens only. Native ETH transfers emit no logs, so an ETH escrow with no refund address and no linked wallet has no discoverable owner. That is exactly why the API refuses to create an ETH dare without one.

Retry and backoff

A refund that fails for a transient reason, an RPC that is down, a sponsor out of gas, a transfer that reverts, is not abandoned. The attempt counter goes up, the error is stored where an admin can read it, and the next attempt is scheduled.

AttemptWaits before the next try
130 seconds
21 minute
32 minutes
44 minutes
58 minutes
616 minutes
732 minutes
8 and after1 hour, the cap

After 12 failed attempts, roughly six and a half hours of trying, the dare stops being retried automatically and is flagged for a human. Some failures skip straight to that state because no number of retries would help: no known refund address, an escrow key that does not decrypt or does not match its address, an unsupported reward token, or an escrow that is empty although it has already sent a transaction, which means the refund went out but was never recorded.

A refund transfer that was broadcast but never confirmed is remembered by hash. The next attempt checks that transaction's receipt before doing anything, so a late confirmation is recorded rather than swept a second time.

What “needs manual refund” means

It means the software has stopped trying and an admin has to act. The dare stays expired, the escrow keeps holding the money, and the last error is shown on the admin page next to the dare. Nothing is lost and nothing is silently written off.

An admin resolves it one of two ways.

ActionWhat it does
Retry refundRuns exactly the same refund step the scheduler uses, ignoring the attempt cap and the backoff and resetting the counter. This is what fixes a refund that failed because of an outage or an empty sponsor wallet.
Record a refund by handThe admin moves the funds themselves and posts the transaction hash, which marks the dare refunded. It is refused while an automatic refund holds the per-dare lock, so a transfer already in flight cannot be overwritten.

Expired acceptances

An acceptance carries the dare's own deadline, so the scheduler expires acceptances at the same moment it expires the dare. Nothing about the dare changes because of it: an expired acceptance never blocks anyone, and other hunters keep whatever time is left.

A hunter whose acceptance expired while the dare is somehow still alive, which happens when an extension granted to someone else pushed the deadline out, can simply accept again. The status endpoint reports them as not accepted with canAccept: true, and a new acceptance is stamped with the new deadline.