

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# AI Agent Guidelines for UniFi MCP Server23This document provides universal rules, workflows, and best practices for all AI coding agents contributing to the UniFi MCP Server project. These guidelines ensure consistency, quality, and security across all AI-assisted development.45## Table of Contents67- [Core Principles](#core-principles)8- [File Structure and Organization](#file-structure-and-organization)9- [Workflow Guidelines](#workflow-guidelines)10- [Do's and Don'ts](#dos-and-donts)11- [Testing Requirements](#testing-requirements)12- [Security Guardrails](#security-guardrails)13- [Code Quality Standards](#code-quality-standards)14- [Documentation Requirements](#documentation-requirements)15- [Approval and Merge Policies](#approval-and-merge-policies)1617## Core Principles1819All AI agents must adhere to these fundamental principles:2021### 1. Safety First2223- Never perform destructive operations without explicit confirmation24- Always validate inputs and handle errors gracefully25- Implement proper authentication and authorization checks26- Never commit or expose sensitive data2728### 2. Clarity and Transparency2930- Write clear, self-documenting code with appropriate comments31- Document all decisions and trade-offs32- Tag AI-generated contributions appropriately33- Explain complex logic in docstrings3435### 3. Consistency3637- Follow the project's established patterns and conventions38- Maintain consistent code style (enforced by linting tools)39- Use consistent naming conventions throughout the codebase40- Adhere to the project's architecture and design patterns4142### 4. Quality Over Speed4344- Prioritize correctness and maintainability over quick delivery45- Include comprehensive tests for all code changes46- Ensure code passes all quality checks before submission47- Perform self-review before requesting human review4849## File Structure and Organization5051### Project Layout5253```54unifi-mcp-server/55├── .github/56│ └── workflows/ # CI/CD pipeline definitions57├── src/58│ ├── __init__.py59│ ├── main.py # MCP server entry point60│ ├── config/ # Configuration management61│ │ ├── __init__.py62│ │ ├── settings.py # Pydantic settings models63│ │ └── config.yaml # Default configuration64│ ├── api/ # UniFi API client65│ │ ├── __init__.py66│ │ ├── client.py # HTTP client wrapper67│ │ └── endpoints.py # API endpoint definitions68│ ├── tools/ # MCP tool definitions69│ │ ├── __init__.py70│ │ ├── devices.py # Device management tools71│ │ ├── networks.py # Network configuration tools72│ │ └── firewall.py # Firewall rule tools73│ ├── resources/ # MCP resource definitions74│ │ ├── __init__.py75│ │ └── schemas.py # Resource URI schemas76│ └── utils/ # Utility functions77│ ├── __init__.py78│ └── validators.py # Input validation helpers79├── tests/80│ ├── __init__.py81│ ├── conftest.py # Pytest fixtures82│ ├── unit/ # Unit tests83│ └── integration/ # Integration tests84├── docs/ # Additional documentation85├── .env.example # Environment variable template86├── .gitignore87├── .aiignore88├── pyproject.toml89├── README.md90└── ...91```9293### File Naming Conventions9495- **Python files:** Use `snake_case` (e.g., `device_manager.py`)96- **Classes:** Use `PascalCase` (e.g., `UniFiClient`)97- **Functions/variables:** Use `snake_case` (e.g., `get_devices()`)98- **Constants:** Use `UPPER_SNAKE_CASE` (e.g., `DEFAULT_TIMEOUT`)99- **Test files:** Prefix with `test_` (e.g., `test_device_manager.py`)100101## Workflow Guidelines102103### Before Starting Work1041051. **Understand the Context:**106 - Read relevant documentation (`README.md`, `API.md`, `CONTRIBUTING.md`)107 - Review related issues and pull requests108 - Understand the feature request or bug report completely1091102. **Plan Your Approach:**111 - Break down the task into smaller, manageable steps112 - Identify affected files and components113 - Consider potential edge cases and error conditions114 - Plan for comprehensive test coverage1151163. **Check Existing Code:**117 - Review similar implementations in the codebase118 - Identify reusable functions and patterns119 - Ensure your approach is consistent with existing code120121### During Development1221231. **Write Code Incrementally:**124 - Implement one feature or fix at a time125 - Test each change before moving to the next126 - Commit logical units of work separately1271282. **Follow TDD (Test-Driven Development):**129 - Write tests first when possible130 - Ensure tests fail before implementing the feature131 - Verify tests pass after implementation132 - Maintain minimum 80% code coverage1331343. **Document as You Go:**135 - Add docstrings to all public functions and classes136 - Update relevant documentation files137 - Add inline comments for complex logic138 - Keep `API.md` updated for new MCP tools/resources139140### After Implementation1411421. **Self-Review:**143 - Review your own code critically144 - Ensure all tests pass: `pytest`145 - Run linting and formatting: `pre-commit run --all-files`146 - Check for security issues: `bandit -r src/`1471482. **Create a Pull Request:**149 - Write a clear, descriptive PR title (conventional commits format)150 - Fill out the PR template completely151 - Link related issues152 - Tag the PR as AI-assisted153 - Request human review1541553. **Respond to Feedback:**156 - Address all review comments157 - Make requested changes promptly158 - Explain decisions when necessary159 - Re-request review after making changes160161## Do's and Don'ts162163### Do's ✅164165- **DO** validate all user inputs166- **DO** handle errors gracefully with try/except blocks167- **DO** use type hints for all function signatures168- **DO** write comprehensive docstrings169- **DO** add tests for all new code170- **DO** use async/await for I/O-bound operations171- **DO** centralize API interactions in dedicated modules172- **DO** use environment variables for configuration173- **DO** log important events and errors appropriately174- **DO** follow the principle of least privilege175- **DO** ask for clarification when requirements are unclear176- **DO** use Pydantic models for data validation177- **DO** keep functions small and focused (single responsibility)178- **DO** reuse existing code when possible179180### Don'ts ❌181182- **DON'T** commit credentials, API keys, or secrets183- **DON'T** merge code without human approval184- **DON'T** skip writing tests185- **DON'T** ignore linting errors or warnings186- **DON'T** expose sensitive information in logs or error messages187- **DON'T** make breaking changes without discussion188- **DON'T** copy-paste code - create reusable functions instead189- **DON'T** use bare except clauses - catch specific exceptions190- **DON'T** hardcode values that should be configurable191- **DON'T** submit incomplete or experimental code to main192- **DON'T** bypass security checks or pre-commit hooks193- **DON'T** write code without understanding its purpose194- **DON'T** use deprecated libraries or functions195- **DON'T** ignore type errors from MyPy196197## Testing Requirements198199### Test Coverage200201All AI-generated code must include tests:202203- **Minimum Coverage:** 80% overall204- **New Features:** 100% coverage of new code paths205- **Bug Fixes:** Regression tests for the fixed bug206- **Refactoring:** Maintain or improve existing coverage207208### Test Types2092101. **Unit Tests:**211212```python213 import pytest214 from src.api.client import UniFiClient215216 @pytest.mark.unit217 async def test_client_initialization():218 """Test that UniFi client initializes correctly."""219 client = UniFiClient(220 api_key="test-api-key",221 api_type="cloud",222 host="api.ui.com"223 )224 assert client.host == "api.ui.com"225 assert client.api_type == "cloud"226 assert client.api_key == "test-api-key"227```2282292. **Integration Tests:**230231```python232 import pytest233234 @pytest.mark.integration235 async def test_get_devices_from_real_api():236 """Test fetching devices from real UniFi Cloud API."""237 # This test requires UNIFI_API_KEY environment variable238 client = UniFiClient.from_env()239 devices = await client.get_devices()240 assert isinstance(devices, list)241```2422433. **Mock Tests:**244245```python246 from unittest.mock import AsyncMock, patch247248 @pytest.mark.unit249 async def test_get_devices_handles_error():250 """Test error handling when API fails."""251 with patch('src.api.client.httpx.AsyncClient.get') as mock_get:252 mock_get.side_effect = httpx.HTTPError("Connection failed")253 client = UniFiClient(254 api_key="test-key",255 host="api.ui.com",256 api_type="cloud"257 )258 with pytest.raises(APIError):259 await client.get_devices()260```261262### Running Tests263264```bash265# Run all tests266pytest267268# Run with coverage269pytest --cov=src --cov-report=html --cov-report=term-missing270271# Run only unit tests272pytest -m unit273274# Run only integration tests (requires UniFi controller)275pytest -m integration276277# Run specific test file278pytest tests/unit/test_client.py279280# Run tests matching pattern281pytest -k "test_device"282```283284## Security Guardrails285286### Credential Management287288**NEVER include API keys or credentials in code:**289290```python291# ❌ BAD - Hardcoded API key292client = UniFiClient(293 api_key="abc123def456ghi789...",294 host="api.ui.com"295)296297# ✅ GOOD - Load from environment298from src.config.settings import Settings299300settings = Settings() # Loads from environment301client = UniFiClient(302 api_key=settings.unifi_api_key,303 host=settings.unifi_host,304 api_type=settings.unifi_api_type305)306```307308### Input Validation309310Always validate and sanitize inputs:311312```python313from pydantic import BaseModel, Field, validator314315class NetworkConfig(BaseModel):316 name: str = Field(..., min_length=1, max_length=32)317 vlan_id: int = Field(..., ge=1, le=4094)318 subnet: str319320 @validator('subnet')321 def validate_subnet(cls, v):322 import ipaddress323 try:324 ipaddress.ip_network(v)325 except ValueError:326 raise ValueError('Invalid subnet format')327 return v328```329330### Error Handling331332Don't expose sensitive information in errors:333334```python335# ❌ BAD - Exposes API key in logs336logging.error(f"Auth failed with API key: {api_key}")337338# ✅ GOOD - Safe error message339logging.error(f"Authentication failed for host '{host}' (API key: {api_key[:8]}...)")340341# ✅ EVEN BETTER - No key exposure342logging.error(f"Authentication failed for host '{host}'. Check your UNIFI_API_KEY.")343```344345### Secret Detection346347Pre-commit hooks will prevent committing secrets:348349```bash350# Initialize pre-commit351pre-commit install352353# Manually check for secrets354detect-secrets scan355```356357## UniFi API Guidelines358359### Authentication with API Keys360361This project uses the **official UniFi Cloud API** with API key authentication. All AI agents must follow these guidelines:362363**Authentication Method:**364365- Use `UNIFI_API_KEY` environment variable for authentication366- API key is passed via the `X-API-Key` HTTP header367- No session management or cookies required (stateless authentication)368369**NEVER hardcode API keys:**370371```python372# ❌ BAD - Hardcoded API key373headers = {374 "X-API-Key": "abc123def456..."375}376377# ✅ GOOD - Load from environment378from src.config.settings import Settings379380settings = Settings()381headers = {382 "X-API-Key": settings.unifi_api_key383}384```385386### API Access Modes387388Support both cloud and local gateway access modes:389390**Cloud API (Default):**391392```python393# Base URL: https://api.ui.com/v1/394settings.unifi_api_type = "cloud"395settings.unifi_host = "api.ui.com"396settings.unifi_port = 443397```398399**Local Gateway Proxy:**400401```python402# Base URL: https://{gateway-ip}/proxy/network/integration/v1/403settings.unifi_api_type = "local"404settings.unifi_host = "192.168.2.1"405settings.unifi_port = 443406```407408### Read-Only Limitation409410**IMPORTANT:** The Early Access API is currently **read-only**.411412```python413# ✅ ALLOWED - Read operations414async def list_devices(site_id: str):415 """List all devices - read operation."""416 devices = await client.get(f"/v1/sites/{site_id}/devices")417 return devices418419# ❌ NOT AVAILABLE - Write operations (will fail)420async def create_network(name: str, vlan_id: int):421 """Create network - not yet supported in EA API."""422 # This will return 403 Forbidden in current API version423 raise NotImplementedError(424 "Write operations are not available in the Early Access API. "425 "This feature will be available in v1 Stable release."426 )427```428429**Handling write requests:**430431- Document that write operations are not yet available432- Return clear error messages to users433- Consider implementing a "preview" mode that shows what would be created434- Monitor UniFi API release notes for v1 Stable availability435436### Rate Limiting Considerations437438Implement proper rate limiting to respect API limits:439440**Current Limits:**441442- Early Access: 100 requests/minute443- v1 Stable (future): 10,000 requests/minute444445**Implementation Example:**446447```python448import asyncio449from collections import deque450from datetime import datetime, timedelta451452class UniFiRateLimiter:453 """Rate limiter for UniFi API requests."""454455 def __init__(self, max_requests: int = 100, window_seconds: int = 60):456 self.max_requests = max_requests457 self.window = timedelta(seconds=window_seconds)458 self.requests = deque()459460 async def acquire(self):461 """Wait if necessary to respect rate limits."""462 now = datetime.now()463464 # Remove old requests outside the window465 while self.requests and self.requests[0] < now - self.window:466 self.requests.popleft()467468 # Wait if at limit469 if len(self.requests) >= self.max_requests:470 sleep_time = (self.requests[0] + self.window - now).total_seconds()471 if sleep_time > 0:472 await asyncio.sleep(sleep_time)473474 self.requests.append(now)475```476477**Best Practices:**478479- Cache frequently accessed data (devices, sites, networks)480- Batch operations when possible481- Implement exponential backoff for 429 errors482- Use configurable rate limits via `UNIFI_RATE_LIMIT` environment variable483- Log rate limit warnings for monitoring484485### API Error Handling486487Handle UniFi API-specific errors gracefully:488489```python490import httpx491from typing import Dict, Any492493class UniFiAPIError(Exception):494 """Base exception for UniFi API errors."""495 pass496497class UniFiAuthenticationError(UniFiAPIError):498 """Authentication failed - invalid API key."""499 pass500501class UniFiRateLimitError(UniFiAPIError):502 """Rate limit exceeded."""503 pass504505async def safe_api_request(506 client: httpx.AsyncClient,507 method: str,508 endpoint: str,509 **kwargs510) -> Dict[str, Any]:511 """512 Make a safe API request with proper error handling.513514 Args:515 client: HTTP client516 method: HTTP method (GET, POST, etc.)517 endpoint: API endpoint518 **kwargs: Additional request parameters519520 Returns:521 Response data as dictionary522523 Raises:524 UniFiAuthenticationError: Invalid API key525 UniFiRateLimitError: Rate limit exceeded526 UniFiAPIError: Other API errors527 """528 try:529 response = await client.request(method, endpoint, **kwargs)530 response.raise_for_status()531 return response.json()532533 except httpx.HTTPStatusError as e:534 if e.response.status_code == 401:535 raise UniFiAuthenticationError(536 "Invalid API key. Please check your UNIFI_API_KEY."537 )538 elif e.response.status_code == 429:539 retry_after = e.response.headers.get("Retry-After", 60)540 raise UniFiRateLimitError(541 f"Rate limit exceeded. Retry after {retry_after} seconds."542 )543 else:544 raise UniFiAPIError(f"API error: {e.response.status_code}")545546 except httpx.RequestError as e:547 raise UniFiAPIError(f"Request failed: {str(e)}")548```549550### Official API Documentation551552Always reference the official UniFi API documentation:553554**Primary Resources:**555556- **Getting Started**: [https://developer.ui.com/site-manager-api/gettingstarted](https://developer.ui.com/site-manager-api/gettingstarted)557- **Project Reference**: `docs/UNIFI_API.md` (comprehensive guide)558- **API Tutorial**: [https://www.makewithdata.tech/p/build-a-mcp-server-for-ai-access](https://www.makewithdata.tech/p/build-a-mcp-server-for-ai-access)559560**When implementing new features:**5615621. Review official API documentation first5632. Check `docs/UNIFI_API.md` for project-specific guidance5643. Ensure endpoint paths match official specifications5654. Test against real UniFi Cloud API or gateway proxy5665. Document any API limitations or quirks discovered567568### API Key Security Checklist569570Before committing code, verify:571572- [ ] No hardcoded API keys in source code573- [ ] API key loaded from environment variables574- [ ] No API keys in log messages (even debug logs)575- [ ] API keys redacted in error messages576- [ ] `.env` file in `.gitignore`577- [ ] `.env.example` has placeholder, not real key578- [ ] Pre-commit hooks detect-secrets passing579- [ ] Documentation mentions API key security580581## Code Quality Standards582583### Type Hints584585All functions must have type hints:586587```python588from typing import List, Dict, Optional589import httpx590591async def get_devices(592 client: httpx.AsyncClient,593 site_id: str = "default"594) -> List[Dict[str, Any]]:595 """Fetch devices from UniFi controller."""596 response = await client.get(f"/api/s/{site_id}/stat/device")597 return response.json()["data"]598```599600### Docstrings601602Use Google-style docstrings:603604```python605def calculate_network_capacity(606 bandwidth_mbps: int,607 devices: int,608 overhead_percent: float = 0.2609) -> float:610 """611 Calculate available bandwidth per device.612613 Args:614 bandwidth_mbps: Total bandwidth in Mbps615 devices: Number of connected devices616 overhead_percent: Network overhead as decimal (default: 0.2 for 20%)617618 Returns:619 Available bandwidth per device in Mbps620621 Raises:622 ValueError: If bandwidth or devices is less than 1623624 Example:625 >>> calculate_network_capacity(100, 10)626 8.0627 """628 if bandwidth_mbps < 1 or devices < 1:629 raise ValueError("Bandwidth and devices must be positive")630631 available = bandwidth_mbps * (1 - overhead_percent)632 return available / devices633```634635### Code Formatting636637Code is automatically formatted by pre-commit hooks:638639```bash640# Format code641black src/ tests/642643# Sort imports644isort src/ tests/645646# Lint code647ruff check src/ tests/ --fix648```649650## Documentation Requirements651652### Code Documentation653654- **All public functions:** Require docstrings655- **Complex logic:** Add inline comments656- **Type hints:** Required for all functions657- **Examples:** Include in docstrings when helpful658659### Project Documentation660661Update relevant documentation files:662663- `README.md` - For user-facing changes664- `API.md` - For new MCP tools or resources665- `CONTRIBUTING.md` - For workflow changes666- `SECURITY.md` - For security-related changes667668### API Documentation669670When adding new MCP tools, document in `API.md`:671672```markdown673### get_device674675Retrieve information about a specific UniFi device.676677**Parameters:**678- `mac_address` (string, required): Device MAC address679- `site_id` (string, optional): Site identifier (default: "default")680681**Returns:**682Object containing device information683684**Example:**685\`\`\`python686result = await mcp.call_tool("get_device", {687 "mac_address": "aa:bb:cc:dd:ee:ff",688 "site_id": "default"689})690\`\`\`691```692693## Approval and Merge Policies694695### Auto-Merge Restrictions696697AI agents **MUST NOT**:698699- Automatically merge pull requests700- Bypass code review requirements701- Push directly to the `main` branch702- Override branch protection rules703- Disable or skip CI/CD checks704705### Required Approvals706707All AI-generated code requires:708709- At least one human reviewer approval710- All CI/CD checks passing711- No unresolved review comments712- Up-to-date with the `main` branch713714### Human-in-the-Loop715716Critical changes require additional human review:717718- Security-related code719- Authentication/authorization logic720- Data deletion or modification721- API contract changes722- Database schema changes723- Configuration changes affecting production724725### Tagging AI Contributions726727Mark AI-assisted PRs in the description:728729```markdown730## AI Assistance731732This PR was created with assistance from Claude Code.733734**Human Review Status:** ✅ Reviewed and approved by @username735**Test Coverage:** 95%736**Security Review:** Completed737```738739## Special Considerations740741### MCP-Specific Guidelines742743When implementing MCP tools and resources:744745```python746from fastmcp import FastMCP747748mcp = FastMCP("UniFi Network")749750@mcp.tool()751async def get_devices(site_id: str = "default") -> list:752 """753 Get all devices for a site.754755 Args:756 site_id: Site identifier (default: "default")757758 Returns:759 List of device objects760 """761 # Implementation762 pass763764@mcp.resource("sites://{site_id}/devices")765async def list_site_devices(site_id: str) -> str:766 """List all devices in a site."""767 # Return JSON string of devices768 pass769```770771### UniFi API Integration772773Centralize API calls in dedicated modules:774775```python776# src/api/client.py777class UniFiClient:778 async def request(779 self,780 method: str,781 endpoint: str,782 **kwargs783 ) -> Dict[str, Any]:784 """785 Make authenticated request to UniFi API.786787 Handles authentication, retries, and error handling.788 """789 # Centralized implementation790 pass791```792793## Conclusion794795By following these guidelines, AI agents can contribute effectively to the UniFi MCP Server project while maintaining high standards of quality, security, and maintainability.796797**Remember:** When in doubt, ask for human guidance. It's better to request clarification than to make assumptions that could introduce bugs or security issues.798799---800801**Last Updated:** 2025-10-17802803For additional guidance, see:804805- `AI_CODING_ASSISTANT.md` - Project-specific AI guidelines806- `AI_GIT_PRACTICES.md` - AI-specific Git practices807- `CONTRIBUTING.md` - General contribution guidelines808- `SECURITY.md` - Security policies809
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| enuno/unifi-mcp-server.claude/skills/fastmcp/CLAUDE.md · 226 | CLAUDE.md | no sections | 25/100 | today | |
| enuno/unifi-mcp-server.clinerules · 226 | Cline rules | setuptestlint-formatstyle+10 | 96/100 | today | |
| enuno/unifi-mcp-server.cursor/rules/common-mistakes.mdc · 226 | Cursor rules | testlint-formatgitdo-not | 93/100 | today | |
| enuno/unifi-mcp-server.cursor/rules/core-principles.mdc · 226 | Cursor rules | teststyletypestesting-strategy+3 | 59/100 | today | |
| enuno/unifi-mcp-server.cursor/rules/environment-setup.mdc · 226 | Cursor rules | setupgitsecurityapi+2 | 57/100 | today | |
| enuno/unifi-mcp-server.cursor/rules/mcp-tools.mdc · 226 | Cursor rules | no sections | 45/100 | today | |
| enuno/unifi-mcp-server.cursor/rules/project-context.mdc · 226 | Cursor rules | testlint-formatstylearch+2 | 73/100 | today | |
| enuno/unifi-mcp-server.cursor/rules/unifi-api.mdc · 226 | Cursor rules | setupsecurityapiui+1 | 62/100 | today | |
| enuno/unifi-mcp-server.cursor/rules/workflow.mdc · 226 | Cursor rules | testlint-formatgitagent-behaviour | 86/100 | today | |
| enuno/unifi-mcp-server.cursorrules · 226 | .cursorrules | setuptestlint-formatstyle+7 | 93/100 | today | |
| enuno/unifi-mcp-serverCLAUDE.md · 226 | CLAUDE.md | testlint-formatarchtesting-strategy+4 | 90/100 | today | |
| enuno/unifi-mcp-serverGEMINI.md · 226 | GEMINI.md | setuptestlint-formatstyle+8 | 89/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| bagisto/bagistoAGENTS.md · 28k | AGENTS.md | setupbuildteststyle+7 | 100/100 | 7 days ago | |
| wpscanteam/wpscanAGENTS.md · 9.7k | AGENTS.md | setupbuildteststyle+6 | 100/100 | 13 days ago | |
| vllm-project/vllmAGENTS.md · 89k | AGENTS.md | setuptestlint-formatstyle+5 | 100/100 | 14 days ago | |
| deepseek-ai/deepseek-harnessnative/landlock-run/AGENTS.md · 104k | AGENTS.md | setupteststylearch+3 | 100/100 | today | |
| ethereum/go-ethereumAGENTS.md · 51k | AGENTS.md | buildtestlint-formatgit+1 | 100/100 | 14 days ago | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 201k | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| aaif-goose/gooseAGENTS.md · 53k | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 8 days ago | |
| unoplat/unoplat-code-confluenceunoplat-code-confluence-frontend/AGENTS.md · 95 | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 13 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/enuno-unifi-mcp-server-agents)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.