From 90571e221b16cbe250bb1abd64c46b5975430974 Mon Sep 17 00:00:00 2001
From: barneyjackson <1398471+barneyjackson@users.noreply.github.com>
Date: Wed, 18 Feb 2026 12:51:51 +0000
Subject: [PATCH] docs(py): automated update of leadr-oss docs bundle (v0.8.8)
---
docs/api/.api-version | 2 +-
docs/api/http-api/authentication.md | 4 +
docs/api/http-api/index.md | 4 +-
docs/api/http-api/scores.md | 2 +-
docs/api/openapi.json | 6 +-
docs/api/reference/accounts.md | 6 +-
docs/api/reference/auth.md | 112 +++++++++++++++++++++++++++-
docs/api/reference/boards.md | 10 +--
docs/api/reference/common.md | 52 +++++++++++++
docs/api/reference/config.md | 15 ++++
docs/api/reference/scores.md | 4 +-
11 files changed, 198 insertions(+), 19 deletions(-)
diff --git a/docs/api/.api-version b/docs/api/.api-version
index 060b616..90d9dc1 100644
--- a/docs/api/.api-version
+++ b/docs/api/.api-version
@@ -1 +1 @@
-v0.8.7
+v0.8.8
diff --git a/docs/api/http-api/authentication.md b/docs/api/http-api/authentication.md
index 6054d9a..32aaa2b 100644
--- a/docs/api/http-api/authentication.md
+++ b/docs/api/http-api/authentication.md
@@ -154,9 +154,13 @@ the device record and creates a new identity session.
No authentication is required to call this endpoint (it IS the authentication).
+The _geo parameter triggers GeoIP lookup for this endpoint. Geo data is
+available for future use via _geo.timezone, _geo.country, _geo.city.
+
Args:
session_request: Session start request with game_id and fingerprint
identity_service: IdentityService dependency (handles device and identity creation)
+ _geo: GeoIP information extracted from client IP address (available for future use)
Returns:
StartSessionResponse with identity info and access tokens
diff --git a/docs/api/http-api/index.md b/docs/api/http-api/index.md
index b093fba..5eb8a2c 100644
--- a/docs/api/http-api/index.md
+++ b/docs/api/http-api/index.md
@@ -1,8 +1,8 @@
---
-title: LEADR Open-Source Edition - Admin & Client API v0.8.6
+title: LEADR Open-Source Edition - Admin & Client API v0.8.7
---
-# LEADR Open-Source Edition - Admin & Client API v0.8.6
+# LEADR Open-Source Edition - Admin & Client API v0.8.7
> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.
diff --git a/docs/api/http-api/scores.md b/docs/api/http-api/scores.md
index 7fe7211..041c2e3 100644
--- a/docs/api/http-api/scores.md
+++ b/docs/api/http-api/scores.md
@@ -459,7 +459,7 @@ are automatically derived from the authenticated session.
Args:
score_request: Score creation details including board_id, player_name, and value.
- request: FastAPI request object for accessing geo data.
+ geo: GeoIP information extracted from client IP address.
service: Injected score service dependency.
board_service: Injected board service for board lookup.
background_tasks: FastAPI background tasks for async metadata updates.
diff --git a/docs/api/openapi.json b/docs/api/openapi.json
index 0e57a5f..b3ef2b0 100644
--- a/docs/api/openapi.json
+++ b/docs/api/openapi.json
@@ -3,7 +3,7 @@
"info": {
"title": "LEADR Open-Source Edition - Admin & Client API",
"description": "LEADR Open-Source Edition is the free cross-platform leaderboard backend for game devs",
- "version": "0.8.6"
+ "version": "0.8.7"
},
"paths": {
"/v1/accounts": {
@@ -7321,7 +7321,7 @@
"Scores"
],
"summary": "Create Score Client",
- "description": "Create a new score (Client API).\n\nCreates a new score submission for a board. All IDs (account_id, game_id, identity_id)\nare automatically derived from the authenticated session.\n\nArgs:\n score_request: Score creation details including board_id, player_name, and value.\n request: FastAPI request object for accessing geo data.\n service: Injected score service dependency.\n board_service: Injected board service for board lookup.\n background_tasks: FastAPI background tasks for async metadata updates.\n auth: Client authentication context with device and identity info.\n pre_create_hook: Hook called before score creation (for quota checks).\n post_create_hook: Hook called after successful score creation.\n\nReturns:\n ScoreClientResponse with the created score (excludes device_id).\n\nRaises:\n 404: Board not found.\n 400: Validation failed (board doesn't belong to account, or game doesn't\n match board's game).\n 403: Score rejected by anti-cheat (rate limit exceeded).",
+ "description": "Create a new score (Client API).\n\nCreates a new score submission for a board. All IDs (account_id, game_id, identity_id)\nare automatically derived from the authenticated session.\n\nArgs:\n score_request: Score creation details including board_id, player_name, and value.\n geo: GeoIP information extracted from client IP address.\n service: Injected score service dependency.\n board_service: Injected board service for board lookup.\n background_tasks: FastAPI background tasks for async metadata updates.\n auth: Client authentication context with device and identity info.\n pre_create_hook: Hook called before score creation (for quota checks).\n post_create_hook: Hook called after successful score creation.\n\nReturns:\n ScoreClientResponse with the created score (excludes device_id).\n\nRaises:\n 404: Board not found.\n 400: Validation failed (board doesn't belong to account, or game doesn't\n match board's game).\n 403: Score rejected by anti-cheat (rate limit exceeded).",
"operationId": "create_score_client_v1_client_scores_post",
"parameters": [
{
@@ -7985,7 +7985,7 @@
"Authentication"
],
"summary": "Start Session",
- "description": "Start a new identity session for a game client.\n\nThis endpoint authenticates game clients and provides JWT access tokens.\nIt is idempotent - calling multiple times for the same fingerprint updates\nthe device record and creates a new identity session.\n\nNo authentication is required to call this endpoint (it IS the authentication).\n\nArgs:\n session_request: Session start request with game_id and fingerprint\n identity_service: IdentityService dependency (handles device and identity creation)\n\nReturns:\n StartSessionResponse with identity info and access tokens\n\nRaises:\n 404: Game not found\n 422: Invalid request (missing required fields, invalid UUID format)",
+ "description": "Start a new identity session for a game client.\n\nThis endpoint authenticates game clients and provides JWT access tokens.\nIt is idempotent - calling multiple times for the same fingerprint updates\nthe device record and creates a new identity session.\n\nNo authentication is required to call this endpoint (it IS the authentication).\n\nThe _geo parameter triggers GeoIP lookup for this endpoint. Geo data is\navailable for future use via _geo.timezone, _geo.country, _geo.city.\n\nArgs:\n session_request: Session start request with game_id and fingerprint\n identity_service: IdentityService dependency (handles device and identity creation)\n _geo: GeoIP information extracted from client IP address (available for future use)\n\nReturns:\n StartSessionResponse with identity info and access tokens\n\nRaises:\n 404: Game not found\n 422: Invalid request (missing required fields, invalid UUID format)",
"operationId": "start_session_v1_client_sessions_post",
"requestBody": {
"content": {
diff --git a/docs/api/reference/accounts.md b/docs/api/reference/accounts.md
index 1977503..03f1018 100644
--- a/docs/api/reference/accounts.md
+++ b/docs/api/reference/accounts.md
@@ -76,7 +76,7 @@ slug: Mapped[str] = mapped_column(String, nullable=False, unique=True, index=Tru
####### `leadr.accounts.adapters.orm.AccountORM.status`
```python
-status: Mapped[AccountStatusEnum] = mapped_column(Enum(AccountStatusEnum, name='account_status', native_enum=True, values_callable=(lambda x: [(e.value) for e in x])), nullable=False, default=(AccountStatusEnum.ACTIVE), server_default='active')
+status: Mapped[AccountStatusEnum] = mapped_column(Enum(AccountStatusEnum, name='account_status', native_enum=True, values_callable=(lambda x: [(e.value) for e in x])), nullable=False, default=(AccountStatusEnum.ACTIVE), server_default='active', index=True)
```
####### `leadr.accounts.adapters.orm.AccountORM.updated_at`
@@ -182,7 +182,7 @@ id: Mapped[uuid_pk]
####### `leadr.accounts.adapters.orm.UserORM.is_owner`
```python
-is_owner: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false')
+is_owner: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false', index=True)
```
####### `leadr.accounts.adapters.orm.UserORM.status`
@@ -194,7 +194,7 @@ status: Mapped[UserStatusEnum] = mapped_column(Enum(UserStatusEnum, name='user_s
####### `leadr.accounts.adapters.orm.UserORM.super_admin`
```python
-super_admin: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false')
+super_admin: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false', index=True)
```
####### `leadr.accounts.adapters.orm.UserORM.updated_at`
diff --git a/docs/api/reference/auth.md b/docs/api/reference/auth.md
index 81176e9..a8ea0bf 100644
--- a/docs/api/reference/auth.md
+++ b/docs/api/reference/auth.md
@@ -1220,7 +1220,7 @@ No authentication is required (the refresh token itself is the credential).
###### `leadr.auth.api.client_routes.start_session`
```python
-start_session(session_request, identity_service)
+start_session(session_request, identity_service, _geo)
```
Start a new identity session for a game client.
@@ -1231,10 +1231,14 @@ the device record and creates a new identity session.
No authentication is required to call this endpoint (it IS the authentication).
+The \_geo parameter triggers GeoIP lookup for this endpoint. Geo data is
+available for future use via \_geo.timezone, \_geo.country, \_geo.city.
+
**Parameters:**
- **session_request** ([StartSessionRequest](#leadr.auth.api.client_schemas.StartSessionRequest)) – Session start request with game_id and fingerprint
- **identity_service** ([IdentityServiceDep](./auth.md#leadr.auth.services.dependencies.IdentityServiceDep)) – IdentityService dependency (handles device and identity creation)
+- **\_geo** ([GeoInfoDep](./common.md#leadr.common.dependencies.GeoInfoDep)) – GeoIP information extracted from client IP address (available for future use)
**Returns:**
@@ -4097,10 +4101,13 @@ and repository layer.
- [**list_all**](#leadr.auth.services.api_key_service.APIKeyService.list_all) – List all non-deleted entities.
- [**list_api_keys**](#leadr.auth.services.api_key_service.APIKeyService.list_api_keys) – List API keys for an account with optional filters and pagination.
- [**record_usage**](#leadr.auth.services.api_key_service.APIKeyService.record_usage) – Record that an API key was used at a specific time.
+- [**record_usage_async**](#leadr.auth.services.api_key_service.APIKeyService.record_usage_async) – Record API key usage asynchronously (for background tasks).
- [**revoke_api_key**](#leadr.auth.services.api_key_service.APIKeyService.revoke_api_key) – Revoke an API key, preventing further use.
+- [**should_update_usage**](#leadr.auth.services.api_key_service.APIKeyService.should_update_usage) – Check if last_used_at needs updating based on configurable threshold.
- [**soft_delete**](#leadr.auth.services.api_key_service.APIKeyService.soft_delete) – Soft-delete an entity and return it before deletion.
- [**update_api_key_status**](#leadr.auth.services.api_key_service.APIKeyService.update_api_key_status) – Update the status of an API key.
- [**validate_api_key**](#leadr.auth.services.api_key_service.APIKeyService.validate_api_key) – Validate an API key and return the domain entity if valid.
+- [**validate_api_key_with_user**](#leadr.auth.services.api_key_service.APIKeyService.validate_api_key_with_user) – Validate an API key and return both the key and associated user.
**Attributes:**
@@ -4343,6 +4350,21 @@ also be called explicitly if needed.
- [EntityNotFoundError](./common.md#leadr.common.domain.exceptions.EntityNotFoundError) – If the key doesn't exist.
+####### `leadr.auth.services.api_key_service.APIKeyService.record_usage_async`
+
+```python
+record_usage_async(key_id)
+```
+
+Record API key usage asynchronously (for background tasks).
+
+This method is designed to be called as a background task. It silently
+handles missing keys to avoid errors in background processing.
+
+**Parameters:**
+
+- **key_id** ([APIKeyID](./common.md#leadr.common.domain.ids.APIKeyID)) – The ID of the API key that was used.
+
####### `leadr.auth.services.api_key_service.APIKeyService.repository`
```python
@@ -4369,6 +4391,27 @@ Revoke an API key, preventing further use.
- [EntityNotFoundError](./common.md#leadr.common.domain.exceptions.EntityNotFoundError) – If the key doesn't exist.
+####### `leadr.auth.services.api_key_service.APIKeyService.should_update_usage`
+
+```python
+should_update_usage(api_key)
+```
+
+Check if last_used_at needs updating based on configurable threshold.
+
+Returns True if:
+
+- last_used_at is None (never used before), or
+- last_used_at is older than API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS
+
+**Parameters:**
+
+- **api_key** ([APIKey](#leadr.auth.domain.api_key.APIKey)) – The API key to check.
+
+**Returns:**
+
+- [bool](#bool) – True if usage should be updated, False otherwise.
+
####### `leadr.auth.services.api_key_service.APIKeyService.soft_delete`
```python
@@ -4427,7 +4470,10 @@ Performs the following checks:
1. Verifies the hash matches
1. Checks if key is active (not revoked)
1. Checks if key is not expired
-1. Records usage timestamp if valid
+
+Note: This method does NOT update last_used_at. The caller should check
+should_update_usage() and schedule record_usage_async() via background task
+if needed.
**Parameters:**
@@ -4448,6 +4494,48 @@ Performs the following checks:
+####### `leadr.auth.services.api_key_service.APIKeyService.validate_api_key_with_user`
+
+```python
+validate_api_key_with_user(plain_key)
+```
+
+Validate an API key and return both the key and associated user.
+
+This method performs a single JOIN query to retrieve both the API key
+and user in one database round-trip, reducing auth latency.
+
+Performs the following checks:
+
+1. Extracts prefix and looks up key + user in database via JOIN
+1. Verifies the hash matches
+1. Checks if key is active (not revoked)
+1. Checks if key is not expired
+
+Note: This method does NOT update last_used_at. The caller should check
+should_update_usage() and schedule record_usage_async() via background task
+if needed.
+
+**Parameters:**
+
+- **plain_key** ([str](#str)) – The plain API key string to validate.
+
+**Returns:**
+
+- [tuple](#tuple)\[[APIKey](#leadr.auth.domain.api_key.APIKey), [User](./accounts.md#leadr.accounts.domain.user.User)\] | None – A tuple of (APIKey, User) if valid, None otherwise.
+
+
+Example
+
+> > > result = await service.validate_api_key_with_user("ldr_abc123...")
+> > > if result:
+> > > ... api_key, user = result
+> > > ... print(f"Valid key for user {user.email}")
+> > > ... else:
+> > > ... print("Invalid or expired key")
+
+
+
##### `leadr.auth.services.dependencies`
Auth service dependency injection factories.
@@ -5715,6 +5803,7 @@ API Key repository for managing API key persistence.
- [**filter**](./auth.md#leadr.auth.services.repositories.APIKeyRepository.filter) – Filter API keys by account and optional criteria with pagination.
- [**get_by_id**](#leadr.auth.services.repositories.APIKeyRepository.get_by_id) – Get an entity by its ID.
- [**get_by_prefix**](#leadr.auth.services.repositories.APIKeyRepository.get_by_prefix) – Get API key by prefix, returns None if not found or soft-deleted.
+- [**get_by_prefix_with_user**](#leadr.auth.services.repositories.APIKeyRepository.get_by_prefix_with_user) – Get API key and associated user in a single query.
- [**update**](./auth.md#leadr.auth.services.repositories.APIKeyRepository.update) – Update an existing entity in the database.
**Attributes:**
@@ -5827,6 +5916,25 @@ get_by_prefix(key_prefix)
Get API key by prefix, returns None if not found or soft-deleted.
+####### `leadr.auth.services.repositories.APIKeyRepository.get_by_prefix_with_user`
+
+```python
+get_by_prefix_with_user(key_prefix)
+```
+
+Get API key and associated user in a single query.
+
+This method performs a JOIN between api_keys and users tables to retrieve
+both entities in one database round-trip, reducing auth latency.
+
+**Parameters:**
+
+- **key_prefix** ([str](#str)) – The API key prefix to search for.
+
+**Returns:**
+
+- [tuple](#tuple)\[[APIKey](#leadr.auth.domain.api_key.APIKey), [User](./accounts.md#leadr.accounts.domain.user.User)\] | None – A tuple of (APIKey, User) if found and neither soft-deleted, None otherwise.
+
####### `leadr.auth.services.repositories.APIKeyRepository.session`
```python
diff --git a/docs/api/reference/boards.md b/docs/api/reference/boards.md
index 3b07877..1e86dcf 100644
--- a/docs/api/reference/boards.md
+++ b/docs/api/reference/boards.md
@@ -96,7 +96,7 @@ created_at: Mapped[timestamp]
####### `leadr.boards.adapters.orm.BoardORM.created_from_template_id`
```python
-created_from_template_id: Mapped[UUID | None] = mapped_column(nullable=True, default=None)
+created_from_template_id: Mapped[UUID | None] = mapped_column(nullable=True, default=None, index=True)
```
####### `leadr.boards.adapters.orm.BoardORM.deleted_at`
@@ -144,13 +144,13 @@ id: Mapped[uuid_pk]
####### `leadr.boards.adapters.orm.BoardORM.is_active`
```python
-is_active: Mapped[bool] = mapped_column(Boolean, nullable=False)
+is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, index=True)
```
####### `leadr.boards.adapters.orm.BoardORM.is_published`
```python
-is_published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=(sa.text('true')))
+is_published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=(sa.text('true')), index=True)
```
####### `leadr.boards.adapters.orm.BoardORM.keep_strategy`
@@ -607,7 +607,7 @@ id: Mapped[uuid_pk]
####### `leadr.boards.adapters.orm.BoardTemplateORM.is_active`
```python
-is_active: Mapped[bool] = mapped_column(Boolean, nullable=False)
+is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, index=True)
```
####### `leadr.boards.adapters.orm.BoardTemplateORM.is_published`
@@ -637,7 +637,7 @@ name_template: Mapped[str | None] = mapped_column(String, nullable=True, default
####### `leadr.boards.adapters.orm.BoardTemplateORM.next_run_at`
```python
-next_run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
+next_run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
```
####### `leadr.boards.adapters.orm.BoardTemplateORM.repeat_interval`
diff --git a/docs/api/reference/common.md b/docs/api/reference/common.md
index ab1bab8..89ef96d 100644
--- a/docs/api/reference/common.md
+++ b/docs/api/reference/common.md
@@ -858,9 +858,15 @@ handles cleanup and rollback on exceptions.
Shared FastAPI dependencies for the application.
+**Functions:**
+
+- [**get_geo_info**](#leadr.common.dependencies.get_geo_info) – FastAPI dependency to get GeoIP info for the request.
+
**Attributes:**
- [**DatabaseSession**](./common.md#leadr.common.dependencies.DatabaseSession) –
+- [**GeoInfoDep**](./common.md#leadr.common.dependencies.GeoInfoDep) –
+- [**logger**](./common.md#leadr.common.dependencies.logger) –
##### `leadr.common.dependencies.DatabaseSession`
@@ -868,6 +874,52 @@ Shared FastAPI dependencies for the application.
DatabaseSession = Annotated[AsyncSession, Depends(get_db)]
```
+##### `leadr.common.dependencies.GeoInfoDep`
+
+```python
+GeoInfoDep = Annotated[GeoInfo, Depends(get_geo_info)]
+```
+
+##### `leadr.common.dependencies.get_geo_info`
+
+```python
+get_geo_info(request)
+```
+
+FastAPI dependency to get GeoIP info for the request.
+
+This dependency performs GeoIP lookup for the client's IP address. It's
+designed to be used on specific endpoints (like score submissions) rather
+than globally as middleware.
+
+The dependency gracefully handles failures - if GeoIP lookup fails for any
+reason, it returns a GeoInfo with all None fields.
+
+**Parameters:**
+
+- **request** ([Request](#fastapi.Request)) – The incoming FastAPI request.
+
+**Returns:**
+
+- [GeoInfo](./common.md#leadr.common.geoip.GeoInfo) – GeoInfo with timezone, country, and city (all may be None).
+
+
+Example
+
+@router.post("/scores")
+async def submit_score(geo: GeoInfoDep):
+timezone = geo.timezone
+country = geo.country
+city = geo.city
+
+
+
+##### `leadr.common.dependencies.logger`
+
+```python
+logger = logging.getLogger(__name__)
+```
+
#### `leadr.common.domain`
**Modules:**
diff --git a/docs/api/reference/config.md b/docs/api/reference/config.md
index a354b2c..1524762 100644
--- a/docs/api/reference/config.md
+++ b/docs/api/reference/config.md
@@ -67,6 +67,7 @@ field names (case-sensitive).
- [**ANTICHEAT_RATE_LIMIT_TIER_C**](#leadr.config.CommonSettings.ANTICHEAT_RATE_LIMIT_TIER_C) ([int](#int)) –
- [**ANTICHEAT_VELOCITY_THRESHOLD_SECONDS**](#leadr.config.CommonSettings.ANTICHEAT_VELOCITY_THRESHOLD_SECONDS) ([float](#float)) –
- [**API_KEY_SECRET**](#leadr.config.CommonSettings.API_KEY_SECRET) ([str](#str)) –
+- [**API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS**](#leadr.config.CommonSettings.API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS) ([int](#int)) –
- [**API_PREFIX**](#leadr.config.CommonSettings.API_PREFIX) ([str](#str)) –
- [**APP**](./config.md#leadr.config.CommonSettings.APP) ([str](#str)) –
- [**BACKGROUND_TASK_EXPIRE_INTERVAL**](#leadr.config.CommonSettings.BACKGROUND_TASK_EXPIRE_INTERVAL) ([int](#int)) –
@@ -196,6 +197,12 @@ ANTICHEAT_VELOCITY_THRESHOLD_SECONDS: float = Field(default=2.0, description='Mi
API_KEY_SECRET: str = Field(default='your-super-secret-api-key-pepper-change-in-production', description='Secret pepper for API key hashing. MUST be changed in production.')
```
+##### `leadr.config.CommonSettings.API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS`
+
+```python
+API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS: int = Field(default=300, description='Only update API key last_used_at if older than this (default: 5 minutes)')
+```
+
##### `leadr.config.CommonSettings.API_PREFIX`
```python
@@ -577,6 +584,7 @@ This is the default settings class used when ENV != 'TEST'.
- [**ANTICHEAT_RATE_LIMIT_TIER_C**](#leadr.config.Settings.ANTICHEAT_RATE_LIMIT_TIER_C) ([int](#int)) –
- [**ANTICHEAT_VELOCITY_THRESHOLD_SECONDS**](#leadr.config.Settings.ANTICHEAT_VELOCITY_THRESHOLD_SECONDS) ([float](#float)) –
- [**API_KEY_SECRET**](#leadr.config.Settings.API_KEY_SECRET) ([str](#str)) –
+- [**API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS**](#leadr.config.Settings.API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS) ([int](#int)) –
- [**API_PREFIX**](#leadr.config.Settings.API_PREFIX) ([str](#str)) –
- [**APP**](#leadr.config.Settings.APP) ([str](#str)) –
- [**BACKGROUND_TASK_EXPIRE_INTERVAL**](#leadr.config.Settings.BACKGROUND_TASK_EXPIRE_INTERVAL) ([int](#int)) –
@@ -664,6 +672,7 @@ Test-specific overrides can be added here.
- [**ANTICHEAT_RATE_LIMIT_TIER_C**](#leadr.config.TestSettings.ANTICHEAT_RATE_LIMIT_TIER_C) ([int](#int)) –
- [**ANTICHEAT_VELOCITY_THRESHOLD_SECONDS**](#leadr.config.TestSettings.ANTICHEAT_VELOCITY_THRESHOLD_SECONDS) ([float](#float)) –
- [**API_KEY_SECRET**](#leadr.config.TestSettings.API_KEY_SECRET) ([str](#str)) –
+- [**API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS**](#leadr.config.TestSettings.API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS) ([int](#int)) –
- [**API_PREFIX**](#leadr.config.TestSettings.API_PREFIX) ([str](#str)) –
- [**APP**](./config.md#leadr.config.TestSettings.APP) ([str](#str)) –
- [**BACKGROUND_TASK_EXPIRE_INTERVAL**](#leadr.config.TestSettings.BACKGROUND_TASK_EXPIRE_INTERVAL) ([int](#int)) –
@@ -793,6 +802,12 @@ ANTICHEAT_VELOCITY_THRESHOLD_SECONDS: float = Field(default=2.0, description='Mi
API_KEY_SECRET: str = Field(default='your-super-secret-api-key-pepper-change-in-production', description='Secret pepper for API key hashing. MUST be changed in production.')
```
+##### `leadr.config.TestSettings.API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS`
+
+```python
+API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS: int = Field(default=300, description='Only update API key last_used_at if older than this (default: 5 minutes)')
+```
+
##### `leadr.config.TestSettings.API_PREFIX`
```python
diff --git a/docs/api/reference/scores.md b/docs/api/reference/scores.md
index 2d15185..0460b3c 100644
--- a/docs/api/reference/scores.md
+++ b/docs/api/reference/scores.md
@@ -995,7 +995,7 @@ client_router = APIRouter()
###### `leadr.scores.api.score_routes.create_score_client`
```python
-create_score_client(score_request, request, service, board_service, background_tasks, auth, identity_service, pre_create_hook, post_create_hook)
+create_score_client(score_request, geo, service, board_service, background_tasks, auth, identity_service, pre_create_hook, post_create_hook)
```
Create a new score (Client API).
@@ -1006,7 +1006,7 @@ are automatically derived from the authenticated session.
**Parameters:**
- **score_request** ([ScoreClientCreateRequest](#leadr.scores.api.score_schemas.ScoreClientCreateRequest)) – Score creation details including board_id, player_name, and value.
-- **request** ([Request](#fastapi.Request)) – FastAPI request object for accessing geo data.
+- **geo** ([GeoInfoDep](./common.md#leadr.common.dependencies.GeoInfoDep)) – GeoIP information extracted from client IP address.
- **service** ([ScoreServiceDep](./scores.md#leadr.scores.services.dependencies.ScoreServiceDep)) – Injected score service dependency.
- **board_service** ([BoardServiceDep](./boards.md#leadr.boards.services.dependencies.BoardServiceDep)) – Injected board service for board lookup.
- **background_tasks** ([BackgroundTasks](#fastapi.BackgroundTasks)) – FastAPI background tasks for async metadata updates.