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.
| # | Step | Scope |
|---|---|---|
| 0 | Check unverified escrows on-chain: activate the funded ones, cancel the empty ones, schedule refunds for late and partial payments. | Newest 200 per tick |
| 1 | Release per-dare refund locks left behind by a process that died. | Locks older than 10 minutes |
| 2 | Expire acceptances whose time is up. | All |
| 3 | Return payouts stuck in processing to approved so an admin can retry them. | Stuck for over 10 minutes |
| 4 | Expire dares whose timer has run out, funded or not. Nothing is deleted. | All |
| 5 | Reopen a dare whose submissions were all rejected while it still has time left. | All |
| 6 | Refund expired, funded dares with nothing left to review. | Oldest 25 per tick |
Dare states
| State | Trigger | Next state |
|---|---|---|
pending_payment | Created | Waiting for funds. The escrow address is shown to the creator. |
pending_payment | Escrow balance covers the reward | active, with the timer starting at that moment |
pending_payment | Empty for more than 1 hour, or the creator cancels | cancelled |
pending_payment | Partly funded and abandoned for more than 24 hours | expired, marked funded, then refunded |
cancelled | Money arrives within 24 hours of creation | expired, marked funded, then refunded. The dare never comes back to life. |
active | First submission arrives | pending_review |
pending_review | Last live submission rejected while time remains | active |
active | Timer runs out | expired |
pending_review | Timer runs out with a submission still live | expired, held for review, not refunded |
pending_review | Admin approves a submission | completed, escrow paid to the winner |
expired | A submission was still under review and the admin approves it | completed, escrow paid to the winner |
expired | No live submissions and the escrow is funded | refunded, escrow swept to the refund destination |
any state except completed or refunded | Admin removes the dare | cancelled if it was never funded, otherwise expired and refunded immediately |
Submission and acceptance states
| State | Trigger | Next state |
|---|---|---|
submission pending | Admin approves | processing |
submission processing | Payout confirmed | approved with a payment hash |
submission processing | Payout failed, or the worker died and 10 minutes passed | approved with no payment hash, waiting for an admin retry |
submission pending | Admin rejects, or another submission won | rejected |
acceptance active | The dare deadline passes | expired |
acceptance active | The hunter submits | submitted |
acceptance active | Admin removes the dare | abandoned |
acceptance expired or abandoned | The hunter accepts again while the dare is still live | active 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.
| Order | Destination | Notes |
|---|---|---|
| 1 | The refund address on the dare | What the creator typed when posting. Required for ETH rewards. |
| 2 | The creator's linked wallet | The wallet on the account, if there is one and it parses as an address. |
| 3 | The recorded payer | The address the server already identified as having funded the escrow. |
| 4 | The payer found on-chain | The 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.
| Attempt | Waits before the next try |
|---|---|
| 1 | 30 seconds |
| 2 | 1 minute |
| 3 | 2 minutes |
| 4 | 4 minutes |
| 5 | 8 minutes |
| 6 | 16 minutes |
| 7 | 32 minutes |
| 8 and after | 1 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.
| Action | What it does |
|---|---|
| Retry refund | Runs 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 hand | The 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.