Index: app/(app)/history/account-filter.tsx
===================================================================
--- app/(app)/history/account-filter.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
+++ app/(app)/history/account-filter.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
@@ -0,0 +1,112 @@
+'use client';
+
+import { useState, useRef, useEffect } from 'react';
+import { useSearchParams, usePathname, useRouter } from 'next/navigation';
+import { WalletIcon, CheckIcon } from '@heroicons/react/24/outline';
+import type { TransactionAccountLite } from '@/app/lib/queries';
+
+export default function AccountFilter({
+    accounts,
+}: {
+    accounts: TransactionAccountLite[];
+}) {
+    const searchParams = useSearchParams();
+    const pathname = usePathname();
+    const { replace } = useRouter();
+    const [open, setOpen] = useState(false);
+    const ref = useRef<HTMLDivElement>(null);
+
+    const currentId = searchParams.get('accountId') ?? '';
+    const hasFilter = currentId !== '';
+
+    // Close when clicking outside
+    useEffect(() => {
+        function onClickOutside(e: MouseEvent) {
+            if (ref.current && !ref.current.contains(e.target as Node)) {
+                setOpen(false);
+            }
+        }
+        if (open) {
+            document.addEventListener('mousedown', onClickOutside);
+            return () => document.removeEventListener('mousedown', onClickOutside);
+        }
+    }, [open]);
+
+    function select(value: string) {
+        const params = new URLSearchParams(searchParams);
+        params.set('page', '1');
+        if (value) {
+            params.set('accountId', value);
+        } else {
+            params.delete('accountId');
+        }
+        replace(`${pathname}?${params.toString()}`);
+        setOpen(false);
+    }
+
+    return (
+        <div ref={ref} className="relative">
+            {/* Icon trigger */}
+            <button
+                type="button"
+                onClick={() => setOpen((v) => !v)}
+                className={`
+                    flex items-center justify-center h-12 w-12 rounded-xl border transition
+                    ${hasFilter
+                        ? 'bg-blue-600 border-blue-500 text-white'
+                        : 'bg-white/15 border-white/15 text-white/60 hover:text-white hover:bg-white/20'
+                    }
+                `}
+                title="Filter by account"
+            >
+                <WalletIcon className="h-5 w-5" />
+            </button>
+
+            {/* Dropdown */}
+            {open && (
+                <div className="absolute right-0 top-14 z-50 w-56 rounded-xl bg-gray-900/95 border border-white/15 backdrop-blur-lg shadow-2xl py-1 overflow-hidden">
+                    <DropdownItem
+                        label="All Accounts"
+                        isSelected={currentId === ''}
+                        onClick={() => select('')}
+                    />
+                    {accounts.map((acc) => {
+                        const id = String(acc.transaction_account_id);
+                        return (
+                            <DropdownItem
+                                key={id}
+                                label={acc.account_name ?? `Account #${acc.transaction_account_id}`}
+                                isSelected={currentId === id}
+                                onClick={() => select(id)}
+                            />
+                        );
+                    })}
+                </div>
+            )}
+        </div>
+    );
+}
+
+function DropdownItem({
+    label,
+    isSelected,
+    onClick,
+}: {
+    label: string;
+    isSelected: boolean;
+    onClick: () => void;
+}) {
+    return (
+        <button
+            type="button"
+            onClick={onClick}
+            className={`
+                w-full flex items-center gap-2 px-4 py-2.5 text-sm text-left transition
+                ${isSelected ? 'text-white bg-white/10' : 'text-white/70 hover:bg-white/5 hover:text-white'}
+            `}
+        >
+            {isSelected && <CheckIcon className="h-4 w-4 shrink-0 text-blue-400" />}
+            <span className={isSelected ? '' : 'pl-6'}>{label}</span>
+        </button>
+    );
+}
Index: app/(app)/history/page.tsx
===================================================================
--- app/(app)/history/page.tsx	(revision e2234a0b0efed1e1cbf2a50b3ea86b8f5475f694)
+++ app/(app)/history/page.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
@@ -1,23 +1,90 @@
+import { auth } from '@/auth';
+import { redirect } from 'next/navigation';
+import { Suspense } from 'react';
 import { poppins } from '@/app/ui/fonts';
+import {
+    getHistoryTransactions,
+    getHistoryTransactionPages,
+    getUserTransactionAccounts,
+    getUserTagsForHistory,
+} from '@/app/lib/queries';
+import Search from './search';
+import AccountFilter from './account-filter';
+import TagFilter from './tag-filter';
+import TransactionList from './transaction-list';
+import Pagination from './pagination';
 
-export default function Page() {
+export default async function HistoryPage(props: {
+    searchParams?: Promise<{
+        query?: string;
+        accountId?: string;
+        tags?: string;
+        page?: string;
+    }>;
+}) {
+    const session = await auth();
+    if (!session?.user?.id) {
+        redirect('/login?callbackUrl=/history');
+    }
+
+    const userId = Number(session.user.id);
+    if (!Number.isInteger(userId)) {
+        redirect('/login?callbackUrl=/history');
+    }
+
+    const searchParams = await props.searchParams;
+    const query = searchParams?.query || '';
+    const accountId = searchParams?.accountId
+        ? Number(searchParams.accountId)
+        : undefined;
+    const selectedTags = searchParams?.tags
+        ? searchParams.tags.split(',').filter(Boolean)
+        : [];
+    const currentPage = Number(searchParams?.page) || 1;
+
+    const [transactions, totalPages, accounts, allTags] = await Promise.all([
+        getHistoryTransactions({ userId, accountId, query, tags: selectedTags, page: currentPage }),
+        getHistoryTransactionPages({ userId, accountId, query, tags: selectedTags }),
+        getUserTransactionAccounts(userId),
+        getUserTagsForHistory(userId),
+    ]);
+
     return (
         <div className="w-full px-6 pt-10 pb-10">
             <h1
                 className={`${poppins.className}
-          text-[40px]
-          leading-tight
-          tracking-tight
-          font-semibold
-          text-center
-          text-white
-        `}
+                    text-[40px]
+                    leading-tight
+                    tracking-tight
+                    font-semibold
+                    text-center
+                    text-white
+                `}
             >
                 History
             </h1>
 
-            <div className="mt-10 rounded-3xl bg-white/5 border border-white/10 p-6 text-white/80">
-                History placeholder
+            <div className="mt-8 flex flex-col gap-3">
+                <div className="flex gap-2 items-center">
+                    <div className="flex-1 min-w-0">
+                        <Suspense fallback={null}>
+                            <Search placeholder="Name, tag, or account…" />
+                        </Suspense>
+                    </div>
+                    <Suspense fallback={null}>
+                        <AccountFilter accounts={accounts} />
+                    </Suspense>
+                </div>
+
+                <Suspense fallback={null}>
+                    <TagFilter tags={allTags} />
+                </Suspense>
             </div>
+
+            <TransactionList transactions={transactions} />
+
+            <Suspense fallback={null}>
+                <Pagination totalPages={totalPages} />
+            </Suspense>
         </div>
     );
Index: app/(app)/history/pagination.tsx
===================================================================
--- app/(app)/history/pagination.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
+++ app/(app)/history/pagination.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
@@ -0,0 +1,122 @@
+'use client';
+
+import { usePathname, useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import clsx from 'clsx';
+import { generatePagination } from '@/app/lib/utils';
+import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
+
+export default function Pagination({ totalPages }: { totalPages: number }) {
+    const pathname = usePathname();
+    const searchParams = useSearchParams();
+    const currentPage = Number(searchParams.get('page')) || 1;
+
+    const allPages = generatePagination(currentPage, totalPages);
+
+    function createPageURL(pageNumber: number | string) {
+        const params = new URLSearchParams(searchParams);
+        params.set('page', pageNumber.toString());
+        return `${pathname}?${params.toString()}`;
+    }
+
+    if (totalPages <= 1) return null;
+
+    return (
+        <div className="mt-6 flex items-center justify-center gap-1">
+            <PaginationArrow
+                direction="left"
+                href={createPageURL(currentPage - 1)}
+                isDisabled={currentPage <= 1}
+            />
+
+            {allPages.map((page, index) => {
+                let position: 'first' | 'last' | 'single' | 'middle' | undefined;
+
+                if (index === 0) position = 'first';
+                if (index === allPages.length - 1) position = 'last';
+                if (allPages.length === 1) position = 'single';
+                if (page === '...') position = 'middle';
+
+                return (
+                    <PaginationNumber
+                        key={`${page}-${index}`}
+                        href={createPageURL(page)}
+                        page={page}
+                        position={position}
+                        isActive={currentPage === page}
+                    />
+                );
+            })}
+
+            <PaginationArrow
+                direction="right"
+                href={createPageURL(currentPage + 1)}
+                isDisabled={currentPage >= totalPages}
+            />
+        </div>
+    );
+}
+
+function PaginationNumber({
+    page,
+    href,
+    isActive,
+    position,
+}: {
+    page: number | string;
+    href: string;
+    position?: 'first' | 'last' | 'middle' | 'single';
+    isActive: boolean;
+}) {
+    const className = clsx(
+        'flex h-9 w-9 items-center justify-center text-sm rounded-lg transition',
+        {
+            'rounded-l-xl': position === 'first' || position === 'single',
+            'rounded-r-xl': position === 'last' || position === 'single',
+            'bg-blue-600 text-white': isActive,
+            'text-white/60 hover:text-white hover:bg-white/10': !isActive && position !== 'middle',
+            'text-white/40 pointer-events-none': position === 'middle',
+        },
+    );
+
+    return isActive || position === 'middle' ? (
+        <div className={className}>{page}</div>
+    ) : (
+        <Link href={href} className={className}>
+            {page}
+        </Link>
+    );
+}
+
+function PaginationArrow({
+    href,
+    direction,
+    isDisabled,
+}: {
+    href: string;
+    direction: 'left' | 'right';
+    isDisabled?: boolean;
+}) {
+    const className = clsx(
+        'flex h-9 w-9 items-center justify-center rounded-lg transition',
+        {
+            'pointer-events-none text-white/20': isDisabled,
+            'text-white/60 hover:text-white hover:bg-white/10': !isDisabled,
+        },
+    );
+
+    const icon =
+        direction === 'left' ? (
+            <ChevronLeftIcon className="h-4 w-4" />
+        ) : (
+            <ChevronRightIcon className="h-4 w-4" />
+        );
+
+    return isDisabled ? (
+        <div className={className}>{icon}</div>
+    ) : (
+        <Link href={href} className={className}>
+            {icon}
+        </Link>
+    );
+}
Index: app/(app)/history/search.tsx
===================================================================
--- app/(app)/history/search.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
+++ app/(app)/history/search.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
@@ -0,0 +1,35 @@
+'use client';
+
+import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
+import { useSearchParams, usePathname, useRouter } from 'next/navigation';
+import { useDebouncedCallback } from 'use-debounce';
+import { poppins } from '@/app/ui/fonts';
+
+export default function Search({ placeholder }: { placeholder: string }) {
+    const searchParams = useSearchParams();
+    const pathname = usePathname();
+    const { replace } = useRouter();
+
+    const handleSearch = useDebouncedCallback((term: string) => {
+        const params = new URLSearchParams(searchParams);
+        params.set('page', '1');
+        if (term) {
+            params.set('query', term);
+        } else {
+            params.delete('query');
+        }
+        replace(`${pathname}?${params.toString()}`);
+    }, 300);
+
+    return (
+        <div className="relative">
+            <MagnifyingGlassIcon className="pointer-events-none absolute left-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-white/60" />
+            <input
+                className={`${poppins.className} w-full h-12 rounded-xl bg-white/15 border border-white/15 pl-11 pr-4 text-white placeholder:text-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500/60 focus:border-blue-500/40 transition`}
+                placeholder={placeholder}
+                onChange={(e) => handleSearch(e.target.value)}
+                defaultValue={searchParams.get('query')?.toString()}
+            />
+        </div>
+    );
+}
Index: app/(app)/history/tag-filter.tsx
===================================================================
--- app/(app)/history/tag-filter.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
+++ app/(app)/history/tag-filter.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
@@ -0,0 +1,62 @@
+'use client';
+
+import { useSearchParams, usePathname, useRouter } from 'next/navigation';
+
+export default function TagFilter({ tags }: { tags: string[] }) {
+    const searchParams = useSearchParams();
+    const pathname = usePathname();
+    const { replace } = useRouter();
+
+    // Read selected tags from URL (?tags=food,transport)
+    const selectedRaw = searchParams.get('tags') ?? '';
+    const selected = selectedRaw ? selectedRaw.split(',').filter(Boolean) : [];
+
+    function toggle(tag: string) {
+        const params = new URLSearchParams(searchParams);
+        params.set('page', '1');
+
+        let next: string[];
+        if (selected.includes(tag)) {
+            next = selected.filter((t) => t !== tag);
+        } else {
+            next = [...selected, tag];
+        }
+
+        if (next.length > 0) {
+            params.set('tags', next.join(','));
+        } else {
+            params.delete('tags');
+        }
+
+        replace(`${pathname}?${params.toString()}`);
+    }
+
+    if (tags.length === 0) return null;
+
+    return (
+        <div className="overflow-x-auto no-scrollbar -mx-6 px-6">
+            <div className="flex gap-2 w-max">
+                {tags.map((tag) => {
+                    const isActive = selected.includes(tag);
+                    return (
+                        <button
+                            key={tag}
+                            type="button"
+                            onClick={() => toggle(tag)}
+                            className={`
+                                whitespace-nowrap rounded-full px-4 py-1.5 text-sm font-medium
+                                border transition-all shrink-0
+                                ${isActive
+                                    ? 'bg-blue-600 border-blue-500 text-white'
+                                    : 'bg-white/10 border-white/10 text-white/60 hover:bg-white/15 hover:text-white/80'
+                                }
+                            `}
+                        >
+                            {tag}
+                        </button>
+                    );
+                })}
+            </div>
+        </div>
+    );
+}
Index: app/(app)/history/transaction-list.tsx
===================================================================
--- app/(app)/history/transaction-list.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
+++ app/(app)/history/transaction-list.tsx	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
@@ -0,0 +1,72 @@
+import { formatMKD, formatDateToLocal } from '@/app/lib/utils';
+import type { HistoryTransaction } from '@/app/lib/queries';
+
+export default function TransactionList({
+    transactions,
+}: {
+    transactions: HistoryTransaction[];
+}) {
+    if (transactions.length === 0) {
+        return (
+            <div className="mt-6 text-center text-white/50 text-sm py-10">
+                No transactions found.
+            </div>
+        );
+    }
+
+    return (
+        <div className="mt-4 space-y-3">
+            {transactions.map((tx) => {
+                const net = Number(tx.net_amount);
+                const isNegative = net < 0;
+
+                // postgres.js may return tags as a parsed array or as a PG
+                // array literal string like "{food,transport}". Normalise to
+                // a plain JS string[] so the pills always render.
+                const tags: string[] = Array.isArray(tx.tags)
+                    ? tx.tags.filter(Boolean)
+                    : typeof tx.tags === 'string' && (tx.tags as string).length > 2
+                      ? (tx.tags as string)
+                            .replace(/^\{|\}$/g, '')
+                            .split(',')
+                            .map((s) => s.trim())
+                            .filter(Boolean)
+                      : [];
+
+                return (
+                    <div
+                        key={tx.transaction_id}
+                        className="rounded-2xl px-5 py-4 bg-black/30 border border-white/10 backdrop-blur-md flex items-center justify-between"
+                    >
+                        <div className="min-w-0 flex-1 mr-3">
+                            <div className="text-white text-lg font-semibold truncate">
+                                {tx.transaction_name ?? 'Transaction'}
+                            </div>
+                            {tags.length > 0 && (
+                                <div className="mt-1.5 flex flex-wrap gap-1.5">
+                                    {tags.map((tag) => (
+                                        <span
+                                            key={tag}
+                                            className="inline-block rounded-full bg-white/15 border border-white/10 px-2.5 py-0.5 text-xs text-white/70"
+                                        >
+                                            {tag}
+                                        </span>
+                                    ))}
+                                </div>
+                            )}
+                            <div className="mt-1 text-xs text-white/40">
+                                {formatDateToLocal(tx.date)}
+                            </div>
+                        </div>
+                        <div
+                            className={`text-xl font-semibold whitespace-nowrap ${isNegative ? 'text-amber-400' : 'text-emerald-300'
+                                }`}
+                        >
+                            {formatMKD(net)}
+                        </div>
+                    </div>
+                );
+            })}
+        </div>
+    );
+}
Index: app/lib/queries.ts
===================================================================
--- app/lib/queries.ts	(revision e2234a0b0efed1e1cbf2a50b3ea86b8f5475f694)
+++ app/lib/queries.ts	(revision f023e5d1535c03dbc322ac4d9c160dfdf983c930)
@@ -25,4 +25,17 @@
 
     recentTransactions: DashboardTransaction[];
+};
+
+export type TransactionAccountLite = {
+    transaction_account_id: number;
+    account_name: string | null;
+};
+
+export type HistoryTransaction = {
+    transaction_id: number;
+    transaction_name: string | null;
+    date: string; // ISO from PG
+    net_amount: string; // numeric comes as string via postgres.js
+    tags: string[]; // aggregated
 };
 
@@ -150,2 +163,144 @@
     };
 }
+
+export async function getUserTransactionAccounts(userId: number) {
+    const rows = await sql<TransactionAccountLite[]>`
+        SELECT
+            ta.transaction_account_id,
+            ta.account_name
+        FROM transaction_account ta
+        WHERE ta.user_id = ${userId}
+        ORDER BY ta.transaction_account_id ASC
+    `;
+    return rows;
+}
+
+export async function getUserTagsForHistory(userId: number) {
+    // Only tags that appear in user's transactions (through breakdown -> account -> user)
+    const rows = await sql<{ tag_name: string }[]>`
+        SELECT DISTINCT tg.tag_name
+        FROM tag tg
+        JOIN tag_assigned_to_transaction tat ON tat.tag_id = tg.tag_id
+        JOIN transaction t ON t.transaction_id = tat.transaction_id
+        JOIN transaction_breakdown tb ON tb.transaction_id = t.transaction_id
+        JOIN transaction_account ta ON ta.transaction_account_id = tb.transaction_account_id
+        WHERE ta.user_id = ${userId}
+        ORDER BY tg.tag_name ASC
+    `;
+    return rows.map((r) => r.tag_name);
+}
+
+export const HISTORY_ITEMS_PER_PAGE = 10;
+
+export async function getHistoryTransactions(params: {
+    userId: number;
+    accountId?: number;
+    query?: string; // searches transaction_name, tag_name, account_name
+    tags?: string[]; // intersection: transaction must have ALL of these tags
+    page?: number;
+}) {
+    const { userId, accountId, query, tags, page = 1 } = params;
+    const offset = (page - 1) * HISTORY_ITEMS_PER_PAGE;
+    const searchPattern = query ? `%${query}%` : null;
+    const tagFilter = tags && tags.length > 0 ? tags : null;
+    const tagCount = tagFilter ? tagFilter.length : 0;
+
+    const rows = await sql<HistoryTransaction[]>`
+        SELECT
+            t.transaction_id,
+            t.transaction_name,
+            t.date::text AS date,
+            (
+                COALESCE(SUM(COALESCE(tb.earned_amount, 0)), 0)
+                -
+                COALESCE(SUM(COALESCE(tb.spent_amount, 0)), 0)
+            )::text AS net_amount,
+            COALESCE(
+                ARRAY_REMOVE(ARRAY_AGG(DISTINCT tg.tag_name), NULL),
+                ARRAY[]::text[]
+            ) AS tags
+        FROM transaction t
+        JOIN transaction_breakdown tb ON tb.transaction_id = t.transaction_id
+        JOIN transaction_account ta ON ta.transaction_account_id = tb.transaction_account_id
+        LEFT JOIN tag_assigned_to_transaction tat ON tat.transaction_id = t.transaction_id
+        LEFT JOIN tag tg ON tg.tag_id = tat.tag_id
+        WHERE ta.user_id = ${userId}
+            AND (${accountId ?? null}::int IS NULL OR tb.transaction_account_id = ${accountId ?? null})
+            AND (
+                ${searchPattern}::text IS NULL
+                OR t.transaction_name ILIKE ${searchPattern}
+                OR ta.account_name ILIKE ${searchPattern}
+                OR EXISTS (
+                    SELECT 1
+                    FROM tag_assigned_to_transaction tat2
+                    JOIN tag tg2 ON tg2.tag_id = tat2.tag_id
+                    WHERE tat2.transaction_id = t.transaction_id
+                        AND tg2.tag_name ILIKE ${searchPattern}
+                )
+            )
+            AND (
+                ${tagCount}::int = 0
+                OR (
+                    SELECT COUNT(DISTINCT tg_f.tag_name)
+                    FROM tag_assigned_to_transaction tat_f
+                    JOIN tag tg_f ON tg_f.tag_id = tat_f.tag_id
+                    WHERE tat_f.transaction_id = t.transaction_id
+                        AND tg_f.tag_name = ANY(${tagFilter ?? []}::text[])
+                ) = ${tagCount}
+            )
+        GROUP BY t.transaction_id
+        ORDER BY t.date DESC
+        LIMIT ${HISTORY_ITEMS_PER_PAGE}
+        OFFSET ${offset}
+    `;
+
+    return rows;
+}
+
+export async function getHistoryTransactionPages(params: {
+    userId: number;
+    accountId?: number;
+    query?: string;
+    tags?: string[];
+}) {
+    const { userId, accountId, query, tags } = params;
+    const searchPattern = query ? `%${query}%` : null;
+    const tagFilter = tags && tags.length > 0 ? tags : null;
+    const tagCount = tagFilter ? tagFilter.length : 0;
+
+    const countResult = await sql<{ count: string }[]>`
+        SELECT COUNT(DISTINCT t.transaction_id)::text AS count
+        FROM transaction t
+        JOIN transaction_breakdown tb ON tb.transaction_id = t.transaction_id
+        JOIN transaction_account ta ON ta.transaction_account_id = tb.transaction_account_id
+        LEFT JOIN tag_assigned_to_transaction tat ON tat.transaction_id = t.transaction_id
+        LEFT JOIN tag tg ON tg.tag_id = tat.tag_id
+        WHERE ta.user_id = ${userId}
+            AND (${accountId ?? null}::int IS NULL OR tb.transaction_account_id = ${accountId ?? null})
+            AND (
+                ${searchPattern}::text IS NULL
+                OR t.transaction_name ILIKE ${searchPattern}
+                OR ta.account_name ILIKE ${searchPattern}
+                OR EXISTS (
+                    SELECT 1
+                    FROM tag_assigned_to_transaction tat2
+                    JOIN tag tg2 ON tg2.tag_id = tat2.tag_id
+                    WHERE tat2.transaction_id = t.transaction_id
+                        AND tg2.tag_name ILIKE ${searchPattern}
+                )
+            )
+            AND (
+                ${tagCount}::int = 0
+                OR (
+                    SELECT COUNT(DISTINCT tg_f.tag_name)
+                    FROM tag_assigned_to_transaction tat_f
+                    JOIN tag tg_f ON tg_f.tag_id = tat_f.tag_id
+                    WHERE tat_f.transaction_id = t.transaction_id
+                        AND tg_f.tag_name = ANY(${tagFilter ?? []}::text[])
+                ) = ${tagCount}
+            )
+    `;
+
+    const totalCount = Number(countResult[0]?.count ?? '0');
+    return Math.ceil(totalCount / HISTORY_ITEMS_PER_PAGE);
+}
