300) { dbnToolsError('Stale intersite timestamp.', 401, 'stale_timestamp'); } $raw = file_get_contents('php://input'); if ($raw === false || $raw === '') { dbnToolsError('Empty body.', 400, 'empty_body'); } $expected = hash_hmac('sha256', $ts . '.' . $raw, $secret); if (!hash_equals($expected, $sig)) { dbnToolsError('Bad intersite signature.', 401, 'bad_signature'); } $data = json_decode($raw, true); if (!is_array($data)) { dbnToolsError('Invalid JSON body.', 400, 'invalid_json'); } $email = strtolower(trim((string)($data['email'] ?? ''))); $userIdHint = isset($data['user_id']) ? (int)$data['user_id'] : 0; $credits = (int)($data['credits'] ?? 25); $source = (string)($data['source'] ?? 'survey'); if ($credits < 1 || $credits > 100) { dbnToolsError('Credits out of range.', 400, 'bad_credits'); } if ($email === '' && $userIdHint <= 0) { dbnToolsError('Need email or user_id.', 400, 'missing_user'); } $db = dbnmDb(); // Resolve user_id from hint or email $userId = $userIdHint; if ($userId <= 0 && $email !== '') { $stmt = $db->prepare('SELECT id FROM users WHERE LOWER(email) = ? LIMIT 1'); $stmt->execute([$email]); $userId = (int)($stmt->fetchColumn() ?: 0); } if ($userId <= 0) { // No account yet — that's fine, dobetternorge.no will magic-link them; we'll award on next login. dbnToolsRespond([ 'ok' => true, 'awarded' => false, 'pending' => true, 'message' => 'User not yet registered — will award on first login.', ]); } // Idempotency: don't double-award if (FreeTier::hasCompletedSurvey($userId)) { dbnToolsRespond([ 'ok' => true, 'awarded' => false, 'reason' => 'already_completed', 'balance' => FreeTier::balance($userId), ]); } FreeTier::ensureRow($userId); $newBalance = FreeTier::awardBonus($userId, $credits, $source); FreeTier::markSurveyCompleted($userId); dbnToolsRespond([ 'ok' => true, 'awarded' => true, 'credits' => $credits, 'balance' => $newBalance, 'user_id' => $userId, ]);