{"record":{"id":"aeb73b51ce47d7c7","repo":"linera-io/linera-protocol","slug":"you-have-already-claimed-tokens-for-this-period","errorCode":null,"errorMessage":"You have already claimed tokens for this period","messagePattern":"You have already claimed tokens for this period","errorType":"validation","errorClass":"async_graphql::Error","httpStatus":null,"severity":"warning","filePath":"linera-faucet/server/src/lib.rs","lineNumber":588,"sourceCode":"                .with_label_values(&[\"daily_no_chain\"])\n                .inc();\n            return Err(Error::new(DAILY_NO_CHAIN_MSG));\n        };\n\n        let now = self.storage.clock().current_time();\n        let period = current_daily_period(initial_claim.timestamp.micros(), now.micros());\n        let last_period = self\n            .faucet_storage\n            .last_daily_claim_period(&owner)\n            .await?\n            .unwrap_or(0);\n\n        if period <= last_period {\n            #[cfg(with_metrics)]\n            metrics::CLAIM_REQUESTS_TOTAL\n                .with_label_values(&[\"daily_limit\"])\n                .inc();\n            return Err(Error::new(DAILY_LIMIT_MSG));\n        }\n\n        self.enqueue_daily_request(\n            owner,\n            initial_claim.chain_id,\n            destination,\n            self.daily_claim_amount,\n            period,\n        )\n        .await\n    }\n\n    async fn enqueue_daily_request(\n        &self,\n        owner: AccountOwner,\n        target_chain_id: ChainId,\n        destination: AccountOwner,\n        amount: Amount,","sourceCodeStart":570,"sourceCodeEnd":606,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-faucet/server/src/lib.rs#L570-L606","documentation":"Each owner may make one daily claim per 24-hour period, where period numbers are computed from the owner's initial claim timestamp (`(now - initial_claim) / 24h`, lib.rs:412-414). The faucet refuses with DAILY_LIMIT_MSG when the current period is <= the last claimed period. The window is anchored to your initial claim time (not midnight), and the initial claim itself counts as a claim in period 0.","triggerScenarios":"A second `dailyClaim` within the same 24h window anchored at the initial claim; a concurrent duplicate that raced past the front-door check and is refused again during batch validation (lib.rs:856-858); automated jobs scheduled more than once a day.","commonSituations":"Cron bots running at fixed clock times that drift against the per-owner anchor; retries after client timeouts where the first attempt actually succeeded; test loops calling `dailyClaim` repeatedly without advancing the clock by 24h.","solutions":["Wait until the next period and retry — query `nextDailyClaim(owner)` for the exact unlock timestamp (initial_claim + (last_period + 1) * 24h)","Track the cooldown client-side: next allowed time = initial claim timestamp + (last claimed period + 1) * 24 hours","Ensure only one in-flight `dailyClaim` per owner (single-flight) so retries don't race the front-door check","In tests, advance the mock clock past 24h (DAILY_PERIOD_MICROS) between daily claims"],"exampleFix":"// before - fire dailyClaim on a fixed cron schedule\nclient.daily_claim(owner, None).await?;\n\n// after - gate on the faucet's own cooldown clock\nlet next = client.next_daily_claim(owner).await?; // Option<Timestamp>\nif let Some(unlock) = next {\n    if unlock > now { /* schedule the claim at `unlock`, not at a fixed time */ }\n}\nclient.daily_claim(owner, None).await?;","handlingStrategy":"validation","validationCode":"// Run before dailyClaim:\n//   query { nextDailyClaim(owner: \"<AccountOwner>\") }\n//     -> Some(timestamp): wait until it is in the past, then claim.\n//     -> null: no initial claim exists yet (claim first).\n// The cooldown is anchored at the owner's initial claim time, not midnight.","typeGuard":"fn is_daily_cooldown(msg: &str) -> bool { msg == \"You have already claimed tokens for this period\" }","tryCatchPattern":"Catch the `dailyClaim` error; on `You have already claimed tokens for this period`, query `nextDailyClaim(owner)` and reschedule the call for that timestamp. Never retry immediately — the refusal is deterministic until the next 24h period.","preventionTips":["Schedule daily claims off `nextDailyClaim(owner)`, not a fixed clock time — windows anchor to each owner's initial claim","Single-flight daily claims per owner so concurrent requests don't race the front-door check","In tests, advance the mock clock by 24h (DAILY_PERIOD_MICROS) between daily claims","After a client timeout, check `nextDailyClaim` before retrying — the timed-out attempt may have succeeded"],"tags":["rate-limit","faucet","daily-claim","cooldown","linera"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}