Skip to main content
The sale rotation feature provides real-time information about champions and skins currently on sale in League of Legends.

Overview

Heimerdinger.lol’s sale rotation tracker:
  • Displays current champion sales with discounted RP prices
  • Shows skin sales with discount percentages
  • Fetches live data from the Boris API
  • Implements 8-hour caching for performance
  • Includes a feature toggle for maintenance mode
  • Provides detailed error handling and logging

Route

The sale rotation uses a single route defined in routes/web.php:61:
Route::get('sale-rotation', static fn () => (new SaleController)->index())->name('sales.index');

Feature Toggle

The sale rotation includes a feature toggle to enable/disable the feature:
When the feature is disabled via SALES_ENABLED=false in the .env file, the page displays a maintenance message instead of attempting to fetch sales data.
// source/app/Http/Controllers/SaleController.php:14-17
if (!config('sales.enabled')) {
    return view('sales.index');
}

Configuration

Set the feature toggle in your .env file:
# Enable or disable fetching live Sale Rotation data from the external API.
# Set to true to enable fetching, false to show the maintenance message.
SALES_ENABLED=false

Sales Data Fetching

The SaleController::index() method (source/app/Http/Controllers/SaleController.php:12-79) fetches and caches sales data:
public function index()
{
    if (!config('sales.enabled')) {
        return view('sales.index');
    }

    try {
        $sales = Cache::remember('sales_data', 60 * 60 * 8, function () {
            $borisUrl = rtrim((string) config('services.boris.url'), '/');
            $borisApiKey = (string) config('services.boris.api_key');

            $headers = [
                'X-API-Key' => $borisApiKey,
            ];

            $championSalesResponse = Http::withHeaders($headers)->get($borisUrl.'/api/champions/sales');
            $skinSalesResponse = Http::withHeaders($headers)->get($borisUrl.'/api/skins/sales');

            // Validation and error handling...
        });
    } catch (\Exception $exception) {
        // Error handling...
    }

    return view('sales.index', ['sales' => $sales]);
}

API Endpoints

The sale controller fetches data from two Boris API endpoints:

Champion Sales

GET /api/champions/salesReturns champions currently on sale

Skin Sales

GET /api/skins/salesReturns skins currently on sale

Authentication

Both endpoints require the X-API-Key header with your Boris API key from config('services.boris.api_key').

Response Format

The API returns sales data in the following format:
{
  "items": [
    {
      "id": 1,
      "sale_rp": 487,
      "percent_off": 35
    }
  ]
}
This is transformed into:
[
    'champion_sales' => [
        [
            'item_id' => 1,
            'rp' => 487,
            'percent_off' => 35
        ]
    ],
    'skin_sales' => [
        [
            'item_id' => 53,
            'rp' => 675,
            'percent_off' => 25
        ]
    ]
]

Caching Strategy

Sales data is cached for 8 hours (28,800 seconds) to balance freshness with API rate limits.
$sales = Cache::remember('sales_data', 60 * 60 * 8, function () {
    // Fetch sales data from API
});

Cache Key

The cache uses a simple key: sales_data

Error Handling

The sale controller implements comprehensive error handling:
1

HTTP Status Validation

Checks if both API requests returned successful status codes (200)
if (! $championSalesResponse->successful() || ! $skinSalesResponse->successful()) {
    Log::error('Boris sales request failed.');
    throw new RuntimeException('Boris sales request failed.');
}
2

Payload Validation

Verifies the response contains the expected items array
if (
    ! isset($championSales['items']) || ! is_array($championSales['items']) ||
    ! isset($skinSales['items']) || ! is_array($skinSales['items'])
) {
    Log::error('Boris sales payload is invalid.');
    throw new RuntimeException('Boris sales payload is invalid.');
}
3

Exception Handling

Returns appropriate HTTP error codes to the user
  • 503 Service Unavailable - When Boris API fails or returns invalid data
  • 500 Internal Server Error - For unexpected errors

Error Logging

All errors are logged with contextual information:
Log::error('Boris sales request failed.', [
    'champions_status' => $championSalesResponse->status(),
    'skins_status' => $skinSalesResponse->status(),
]);
Errors are also sent to Discord webhooks if configured (see LOG_DISCORD_WEBHOOK_URL in .env).

Configuration Requirements

To use the sale rotation feature, configure these environment variables:
SALES_ENABLED
boolean
default:"false"
Enable/disable the sale rotation feature
BORIS_URL
string
required
Base URL for the Boris API (e.g., https://boris.heimerdinger.lol)
BORIS_API_KEY
string
required
API key for authenticating with the Boris API

Maintenance Mode

When SALES_ENABLED=false, the sale rotation page displays a maintenance message instead of attempting to fetch data:
if (!config('sales.enabled')) {
    return view('sales.index');
}
The view is returned without any sales data, allowing the template to display a user-friendly maintenance message.

Champions

Browse all champions to see which ones are on sale

Skins

View the full skin catalog with rarity information

Data Sources

Learn about the Boris API and data fetching strategy

Controller API

View the full SaleController API reference

Build docs developers (and LLMs) love