diff --git a/services/modpack-version-checker/blueprint-extension/admin/controller.php b/services/modpack-version-checker/blueprint-extension/admin/controller.php
new file mode 100644
index 0000000..b36a277
--- /dev/null
+++ b/services/modpack-version-checker/blueprint-extension/admin/controller.php
@@ -0,0 +1,82 @@
+blueprint->dbGet('modpackchecker', 'curseforge_api_key');
+ $discord_webhook_url = $this->blueprint->dbGet('modpackchecker', 'discord_webhook_url');
+ $check_interval = $this->blueprint->dbGet('modpackchecker', 'check_interval');
+ $tier = $this->blueprint->dbGet('modpackchecker', 'tier');
+
+ // Set defaults if empty
+ if ($check_interval == '') {
+ $this->blueprint->dbSet('modpackchecker', 'check_interval', 'daily');
+ $check_interval = 'daily';
+ }
+ if ($tier == '') {
+ $this->blueprint->dbSet('modpackchecker', 'tier', 'standard');
+ $tier = 'standard';
+ }
+
+ return $this->view->make(
+ 'admin.extensions.modpackchecker.index', [
+ 'curseforge_api_key' => $curseforge_api_key,
+ 'discord_webhook_url' => $discord_webhook_url,
+ 'check_interval' => $check_interval,
+ 'tier' => $tier,
+ 'root' => '/admin/extensions/modpackchecker',
+ 'blueprint' => $this->blueprint,
+ ]
+ );
+ }
+
+ /**
+ * @throws \Pterodactyl\Exceptions\Model\DataValidationException
+ * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
+ */
+ public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse
+ {
+ $this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key') ?? '');
+ $this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? '');
+ $this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily');
+
+ return redirect()->route('admin.extensions.modpackchecker.index')->with('success', 'Settings saved successfully.');
+ }
+}
+
+class modpackcheckerSettingsFormRequest extends AdminFormRequest
+{
+ public function rules(): array
+ {
+ return [
+ 'curseforge_api_key' => 'nullable|string|max:500',
+ 'discord_webhook_url' => 'nullable|url|max:500',
+ 'check_interval' => 'required|in:daily,12h,6h',
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'curseforge_api_key' => 'CurseForge API Key',
+ 'discord_webhook_url' => 'Discord Webhook URL',
+ 'check_interval' => 'Check Interval',
+ ];
+ }
+}
diff --git a/services/modpack-version-checker/blueprint-extension/admin/view.blade.php b/services/modpack-version-checker/blueprint-extension/admin/view.blade.php
new file mode 100644
index 0000000..c90c87b
--- /dev/null
+++ b/services/modpack-version-checker/blueprint-extension/admin/view.blade.php
@@ -0,0 +1,208 @@
+
diff --git a/services/modpack-version-checker/blueprint-extension/conf.yml b/services/modpack-version-checker/blueprint-extension/conf.yml
new file mode 100644
index 0000000..86158cc
--- /dev/null
+++ b/services/modpack-version-checker/blueprint-extension/conf.yml
@@ -0,0 +1,37 @@
+info:
+ name: "ModpackChecker"
+ identifier: "modpackchecker"
+ description: "4-platform modpack version checker - supports CurseForge, Modrinth, Technic, and FTB"
+ flags: ""
+ version: "1.0.0"
+ target: "beta-2026-01"
+ author: "Firefrost Gaming "
+ icon: ""
+ website: "https://firefrostgaming.com"
+
+admin:
+ view: "admin/view.blade.php"
+ controller: "admin/controller.php"
+ css: ""
+ wrapper: ""
+
+dashboard:
+ css: ""
+ wrapper: "views/server/wrapper.tsx"
+ components: ""
+
+data:
+ directory: "modpackchecker"
+ public: ""
+ console: ""
+
+requests:
+ views: "views"
+ app: ""
+ routers:
+ application: ""
+ client: "routes/client.php"
+ web: ""
+
+database:
+ migrations: "database/migrations"
diff --git a/services/modpack-version-checker/blueprint-extension/controllers/ModpackAPIController.php b/services/modpack-version-checker/blueprint-extension/controllers/ModpackAPIController.php
new file mode 100644
index 0000000..524ecba
--- /dev/null
+++ b/services/modpack-version-checker/blueprint-extension/controllers/ModpackAPIController.php
@@ -0,0 +1,315 @@
+getEggVariable($server, 'MODPACK_PLATFORM');
+ $modpackId = $this->getEggVariable($server, 'MODPACK_ID');
+
+ // Also check platform-specific variables
+ if (empty($modpackId)) {
+ if ($curseforgeId = $this->getEggVariable($server, 'CURSEFORGE_ID')) {
+ $platform = 'curseforge';
+ $modpackId = $curseforgeId;
+ } elseif ($modrinthId = $this->getEggVariable($server, 'MODRINTH_PROJECT_ID')) {
+ $platform = 'modrinth';
+ $modpackId = $modrinthId;
+ } elseif ($ftbId = $this->getEggVariable($server, 'FTB_MODPACK_ID')) {
+ $platform = 'ftb';
+ $modpackId = $ftbId;
+ } elseif ($technicSlug = $this->getEggVariable($server, 'TECHNIC_SLUG')) {
+ $platform = 'technic';
+ $modpackId = $technicSlug;
+ }
+ }
+
+ // 2. Fallback to file fingerprinting if not set
+ if (empty($platform) || empty($modpackId)) {
+ $detected = $this->detectFromFiles($server);
+ if (!empty($detected['platform'])) {
+ $platform = $detected['platform'];
+ $modpackId = $detected['id'] ?? null;
+ }
+ }
+
+ // 3. If still nothing, return unknown
+ if (empty($platform) || empty($modpackId)) {
+ return response()->json([
+ 'success' => false,
+ 'status' => 'unknown',
+ 'message' => 'Could not detect modpack. Set MODPACK_PLATFORM and MODPACK_ID in startup variables.',
+ ]);
+ }
+
+ // 4. Query the appropriate API
+ $versionData = $this->checkVersion($platform, $modpackId);
+
+ return response()->json([
+ 'success' => true,
+ 'server_uuid' => $server->uuid,
+ 'platform' => $platform,
+ 'modpack_id' => $modpackId,
+ 'modpack_name' => $versionData['name'] ?? 'Unknown',
+ 'current_version' => $versionData['current'] ?? 'Unknown',
+ 'latest_version' => $versionData['latest'] ?? 'Unknown',
+ 'status' => $versionData['status'] ?? 'unknown',
+ 'error' => $versionData['error'] ?? null,
+ ]);
+ }
+
+ /**
+ * Get an egg variable value for a server
+ */
+ private function getEggVariable(Server $server, string $name): ?string
+ {
+ $variable = $server->variables()
+ ->whereHas('variable', function ($query) use ($name) {
+ $query->where('env_variable', $name);
+ })
+ ->first();
+
+ return $variable?->variable_value;
+ }
+
+ /**
+ * Attempt to detect modpack from files
+ */
+ private function detectFromFiles(Server $server): array
+ {
+ // Try CurseForge manifest.json
+ try {
+ $content = $this->fileRepository->setServer($server)->getContent('/manifest.json');
+ $json = json_decode($content, true);
+ if (isset($json['projectID'])) {
+ return [
+ 'platform' => 'curseforge',
+ 'id' => (string) $json['projectID'],
+ ];
+ }
+ } catch (\Exception $e) {
+ // File doesn't exist or Wings unreachable
+ }
+
+ // Try Modrinth modrinth.index.json
+ try {
+ $content = $this->fileRepository->setServer($server)->getContent('/modrinth.index.json');
+ $json = json_decode($content, true);
+ if (isset($json['name'])) {
+ // Modrinth index doesn't contain project ID directly
+ // We detect it's Modrinth but need manual ID
+ return [
+ 'platform' => 'modrinth',
+ 'id' => null,
+ 'name' => $json['name'] ?? null,
+ ];
+ }
+ } catch (\Exception $e) {
+ // File doesn't exist
+ }
+
+ return [];
+ }
+
+ /**
+ * Check version against platform API
+ */
+ private function checkVersion(string $platform, string $modpackId): array
+ {
+ return match ($platform) {
+ 'curseforge' => $this->checkCurseForge($modpackId),
+ 'modrinth' => $this->checkModrinth($modpackId),
+ 'technic' => $this->checkTechnic($modpackId),
+ 'ftb' => $this->checkFTB($modpackId),
+ default => ['status' => 'error', 'error' => 'Unknown platform: ' . $platform],
+ };
+ }
+
+ /**
+ * Check CurseForge API
+ */
+ private function checkCurseForge(string $modpackId): array
+ {
+ $apiKey = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
+
+ if (empty($apiKey)) {
+ return [
+ 'status' => 'error',
+ 'error' => 'CurseForge API key not configured. Add it in Admin > Extensions > ModpackChecker.',
+ ];
+ }
+
+ try {
+ $response = Http::withHeaders([
+ 'x-api-key' => $apiKey,
+ 'Accept' => 'application/json',
+ ])->timeout(10)->get("https://api.curseforge.com/v1/mods/{$modpackId}");
+
+ if ($response->status() === 401 || $response->status() === 403) {
+ return ['status' => 'error', 'error' => 'Invalid CurseForge API key.'];
+ }
+
+ if ($response->status() === 404) {
+ return ['status' => 'error', 'error' => 'Modpack not found or delisted.'];
+ }
+
+ if (!$response->successful()) {
+ return ['status' => 'error', 'error' => 'CurseForge API error: ' . $response->status()];
+ }
+
+ $data = $response->json();
+ $name = $data['data']['name'] ?? 'Unknown';
+ $latestFile = $data['data']['latestFiles'][0] ?? null;
+ $latestVersion = $latestFile['displayName'] ?? 'Unknown';
+
+ return [
+ 'name' => $name,
+ 'latest' => $latestVersion,
+ 'status' => 'up_to_date', // Can't determine current without server-side tracking
+ ];
+ } catch (\Exception $e) {
+ return ['status' => 'error', 'error' => 'Failed to connect to CurseForge: ' . $e->getMessage()];
+ }
+ }
+
+ /**
+ * Check Modrinth API
+ */
+ private function checkModrinth(string $modpackId): array
+ {
+ try {
+ // First get project info
+ $projectResponse = Http::withHeaders([
+ 'User-Agent' => 'ModpackChecker/1.0.0 (firefrostgaming.com)',
+ ])->timeout(10)->get("https://api.modrinth.com/v2/project/{$modpackId}");
+
+ if ($projectResponse->status() === 404) {
+ return ['status' => 'error', 'error' => 'Modpack not found on Modrinth.'];
+ }
+
+ if (!$projectResponse->successful()) {
+ return ['status' => 'error', 'error' => 'Modrinth API error: ' . $projectResponse->status()];
+ }
+
+ $projectData = $projectResponse->json();
+ $name = $projectData['title'] ?? 'Unknown';
+
+ // Get versions
+ $versionResponse = Http::withHeaders([
+ 'User-Agent' => 'ModpackChecker/1.0.0 (firefrostgaming.com)',
+ ])->timeout(10)->get("https://api.modrinth.com/v2/project/{$modpackId}/version");
+
+ if (!$versionResponse->successful()) {
+ return [
+ 'name' => $name,
+ 'latest' => 'Unknown',
+ 'status' => 'error',
+ 'error' => 'Could not fetch versions.',
+ ];
+ }
+
+ $versions = $versionResponse->json();
+ $latestVersion = $versions[0]['version_number'] ?? 'Unknown';
+
+ return [
+ 'name' => $name,
+ 'latest' => $latestVersion,
+ 'status' => 'up_to_date',
+ ];
+ } catch (\Exception $e) {
+ return ['status' => 'error', 'error' => 'Failed to connect to Modrinth: ' . $e->getMessage()];
+ }
+ }
+
+ /**
+ * Check Technic API
+ */
+ private function checkTechnic(string $slug): array
+ {
+ try {
+ $response = Http::timeout(10)
+ ->get("https://api.technicpack.net/modpack/{$slug}?build=1");
+
+ if ($response->status() === 404) {
+ return ['status' => 'error', 'error' => 'Modpack not found on Technic.'];
+ }
+
+ if (!$response->successful()) {
+ return ['status' => 'error', 'error' => 'Technic API error: ' . $response->status()];
+ }
+
+ $data = $response->json();
+
+ if (isset($data['error'])) {
+ return ['status' => 'error', 'error' => $data['error']];
+ }
+
+ $name = $data['displayName'] ?? $data['name'] ?? 'Unknown';
+ $latestVersion = $data['version'] ?? 'Unknown';
+
+ return [
+ 'name' => $name,
+ 'latest' => $latestVersion,
+ 'status' => 'up_to_date',
+ ];
+ } catch (\Exception $e) {
+ return ['status' => 'error', 'error' => 'Failed to connect to Technic: ' . $e->getMessage()];
+ }
+ }
+
+ /**
+ * Check FTB (modpacks.ch) API
+ */
+ private function checkFTB(string $modpackId): array
+ {
+ try {
+ $response = Http::timeout(10)
+ ->get("https://api.modpacks.ch/public/modpack/{$modpackId}");
+
+ if ($response->status() === 404) {
+ return ['status' => 'error', 'error' => 'Modpack not found on FTB.'];
+ }
+
+ if (!$response->successful()) {
+ return ['status' => 'error', 'error' => 'FTB API error: ' . $response->status()];
+ }
+
+ $data = $response->json();
+
+ if (isset($data['status']) && $data['status'] === 'error') {
+ return ['status' => 'error', 'error' => $data['message'] ?? 'Unknown FTB error'];
+ }
+
+ $name = $data['name'] ?? 'Unknown';
+ $versions = $data['versions'] ?? [];
+ $latestVersion = !empty($versions) ? ($versions[0]['name'] ?? 'Unknown') : 'Unknown';
+
+ return [
+ 'name' => $name,
+ 'latest' => $latestVersion,
+ 'status' => 'up_to_date',
+ ];
+ } catch (\Exception $e) {
+ return ['status' => 'error', 'error' => 'Failed to connect to FTB: ' . $e->getMessage()];
+ }
+ }
+}
diff --git a/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_06_000000_create_modpackchecker_servers_table.php b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_06_000000_create_modpackchecker_servers_table.php
new file mode 100644
index 0000000..baf2c58
--- /dev/null
+++ b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_06_000000_create_modpackchecker_servers_table.php
@@ -0,0 +1,44 @@
+dbGet/dbSet methods
+ // which store data in the existing settings table.
+ // This migration creates a table for per-server modpack tracking.
+
+ Schema::create('modpackchecker_servers', function (Blueprint $table) {
+ $table->id();
+ $table->string('server_uuid')->unique();
+ $table->string('platform')->nullable(); // curseforge, modrinth, technic, ftb
+ $table->string('modpack_id')->nullable();
+ $table->string('modpack_name')->nullable();
+ $table->string('current_version')->nullable();
+ $table->string('latest_version')->nullable();
+ $table->enum('status', ['up_to_date', 'update_available', 'error', 'unknown'])->default('unknown');
+ $table->timestamp('last_checked')->nullable();
+ $table->text('error_message')->nullable();
+ $table->timestamps();
+
+ // Index for efficient lookups
+ $table->index('status');
+ $table->index('last_checked');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('modpackchecker_servers');
+ }
+};
diff --git a/services/modpack-version-checker/blueprint-extension/routes/client.php b/services/modpack-version-checker/blueprint-extension/routes/client.php
new file mode 100644
index 0000000..64b5509
--- /dev/null
+++ b/services/modpack-version-checker/blueprint-extension/routes/client.php
@@ -0,0 +1,16 @@
+ {
+ const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
+ const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
+ const [data, setData] = useState(null);
+
+ const checkForUpdates = async () => {
+ if (!uuid) return;
+
+ setStatus('loading');
+ try {
+ const response = await http.post(`/api/client/servers/${uuid}/ext/modpackchecker/check`);
+ setData(response.data);
+ setStatus(response.data.success ? 'success' : 'error');
+ } catch (error: any) {
+ setData({
+ success: false,
+ error: error.response?.data?.message || 'Failed to check for updates',
+ });
+ setStatus('error');
+ }
+ };
+
+ const getPlatformIcon = (platform?: string) => {
+ switch (platform) {
+ case 'curseforge':
+ return '🔥';
+ case 'modrinth':
+ return '🌿';
+ case 'technic':
+ return '⚙️';
+ case 'ftb':
+ return '📦';
+ default:
+ return '❓';
+ }
+ };
+
+ const getStatusColor = (status?: string) => {
+ switch (status) {
+ case 'up_to_date':
+ return '#4ECDC4'; // Frost
+ case 'update_available':
+ return '#FF6B35'; // Fire
+ case 'error':
+ return '#ef4444';
+ default:
+ return '#888';
+ }
+ };
+
+ return (
+
+
+
+ Modpack Version
+
+ {status === 'idle' && (
+
+ )}
+ {status === 'success' && (
+
+ )}
+
+
+ {status === 'loading' && (
+
+
+ Checking version...
+
+
+ )}
+
+ {status === 'success' && data?.success && (
+
+
+ {getPlatformIcon(data.platform)}
+ {data.modpack_name}
+
+ ({data.platform})
+
+
+
+
+ Latest:
+ {data.latest_version}
+
+
+
+ )}
+
+ {(status === 'error' || (status === 'success' && !data?.success)) && (
+
+ {data?.error || data?.message || 'Unknown error'}
+
+ )}
+
+
+
+ );
+};
+
+export default ModpackVersionCard;