diff --git a/backend/app/modules/common/time.py b/backend/app/modules/common/time.py
index a7738c7a8f..7080afb08a 100644
--- a/backend/app/modules/common/time.py
+++ b/backend/app/modules/common/time.py
@@ -21,7 +21,10 @@ def timestamp_s(self) -> float:
         return self.timestamp_us() / 10**6
 
     def datetime(self) -> DateTime:
-        return DateTime.fromtimestamp(self.timestamp_s())
+        # Make sure the timestamp is in UTC and not local
+        utc_timestamp = DateTime.fromtimestamp(self.timestamp_s(), timezone.utc)
+        # Remove timezone info
+        return utc_timestamp.replace(tzinfo=None)
 
     def to_isoformat(self):
         return self.datetime().isoformat()
diff --git a/backend/tests/v2/factories/__init__.py b/backend/tests/v2/factories/__init__.py
index 561003f34f..0ef06a28a1 100644
--- a/backend/tests/v2/factories/__init__.py
+++ b/backend/tests/v2/factories/__init__.py
@@ -4,6 +4,10 @@
 from tests.v2.factories.allocations import AllocationFactorySet
 from tests.v2.factories.projects_details import ProjectsDetailsFactorySet
 from tests.v2.factories.users import UserFactorySet
+from tests.v2.factories.budgets import BudgetFactorySet
+from tests.v2.factories.pending_snapshots import PendingEpochSnapshotFactorySet
+from tests.v2.factories.finalized_snapshots import FinalizedEpochSnapshotFactorySet
+from tests.v2.factories.patrons import PatronModeEventFactorySet
 from dataclasses import dataclass
 
 
@@ -23,6 +27,10 @@ class FactoriesAggregator:
     allocation_requests: AllocationRequestFactorySet
     allocations: AllocationFactorySet
     projects_details: ProjectsDetailsFactorySet
+    budgets: BudgetFactorySet
+    pending_snapshots: PendingEpochSnapshotFactorySet
+    finalized_snapshots: FinalizedEpochSnapshotFactorySet
+    patrons: PatronModeEventFactorySet
 
     def __init__(self, fast_session: AsyncSession):
         """
@@ -32,3 +40,7 @@ def __init__(self, fast_session: AsyncSession):
         self.allocation_requests = AllocationRequestFactorySet(fast_session)
         self.allocations = AllocationFactorySet(fast_session)
         self.projects_details = ProjectsDetailsFactorySet(fast_session)
+        self.budgets = BudgetFactorySet(fast_session)
+        self.pending_snapshots = PendingEpochSnapshotFactorySet(fast_session)
+        self.finalized_snapshots = FinalizedEpochSnapshotFactorySet(fast_session)
+        self.patrons = PatronModeEventFactorySet(fast_session)
diff --git a/backend/tests/v2/factories/budgets.py b/backend/tests/v2/factories/budgets.py
new file mode 100644
index 0000000000..dbb769ca75
--- /dev/null
+++ b/backend/tests/v2/factories/budgets.py
@@ -0,0 +1,55 @@
+import random
+from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
+from factory import LazyAttribute
+
+from app.infrastructure.database.models import Budget, User
+from tests.v2.factories.base import FactorySetBase
+from tests.v2.factories.users import UserFactorySet
+from v2.core.types import Address, BigInteger
+
+
+class BudgetFactory(AsyncSQLAlchemyFactory):
+    class Meta:
+        model = Budget
+        sqlalchemy_session_persistence = "commit"
+
+    user_id = None
+    epoch = None
+    budget = LazyAttribute(
+        lambda _: str(random.randint(1, 1000) * 10**18)
+    )  # Random amount in wei
+
+
+class BudgetFactorySet(FactorySetBase):
+    _factories = {"budget": BudgetFactory}
+
+    async def create(
+        self,
+        user: User | Address,
+        epoch: int,
+        amount: BigInteger | None = None,
+    ) -> Budget:
+        """
+        Create a budget for a user in a specific epoch.
+
+        Args:
+            user: The user or user address to create the budget for
+            epoch: The epoch number
+            amount: Optional specific amount, otherwise random
+
+        Returns:
+            The created budget
+        """
+        if not isinstance(user, User):
+            user = await UserFactorySet(self.session).get_or_create(user)
+
+        factory_kwargs = {
+            "user_id": user.id,
+            "epoch": epoch,
+        }
+
+        if amount is not None:
+            factory_kwargs["budget"] = str(amount)
+
+        budget = await BudgetFactory.create(**factory_kwargs)
+        return budget
diff --git a/backend/tests/v2/factories/finalized_snapshots.py b/backend/tests/v2/factories/finalized_snapshots.py
new file mode 100644
index 0000000000..0a2ee9713d
--- /dev/null
+++ b/backend/tests/v2/factories/finalized_snapshots.py
@@ -0,0 +1,67 @@
+import random
+from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
+from factory import LazyAttribute
+
+from app.infrastructure.database.models import FinalizedEpochSnapshot
+from tests.v2.factories.base import FactorySetBase
+from v2.core.types import BigInteger
+
+
+class FinalizedEpochSnapshotFactory(AsyncSQLAlchemyFactory):
+    class Meta:
+        model = FinalizedEpochSnapshot
+        sqlalchemy_session_persistence = "commit"
+
+    epoch = None
+    matched_rewards = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
+    patrons_rewards = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
+    leftover = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))
+    withdrawals_merkle_root = LazyAttribute(
+        lambda _: "0x" + "".join(random.choices("0123456789abcdef", k=64))
+    )
+    total_withdrawals = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
+
+
+class FinalizedEpochSnapshotFactorySet(FactorySetBase):
+    _factories = {"finalized_snapshot": FinalizedEpochSnapshotFactory}
+
+    async def create(
+        self,
+        epoch: int,
+        matched_rewards: BigInteger | None = None,
+        patrons_rewards: BigInteger | None = None,
+        leftover: BigInteger | None = None,
+        withdrawals_merkle_root: str | None = None,
+        total_withdrawals: BigInteger | None = None,
+    ) -> FinalizedEpochSnapshot:
+        """
+        Create a finalized epoch snapshot.
+
+        Args:
+            epoch: The epoch number
+            matched_rewards: Optional matched rewards amount
+            patrons_rewards: Optional patrons rewards amount
+            leftover: Optional leftover amount
+            withdrawals_merkle_root: Optional withdrawals merkle root
+            total_withdrawals: Optional total withdrawals amount
+
+        Returns:
+            The created finalized epoch snapshot
+        """
+        factory_kwargs = {
+            "epoch": epoch,
+        }
+
+        if matched_rewards is not None:
+            factory_kwargs["matched_rewards"] = str(matched_rewards)
+        if patrons_rewards is not None:
+            factory_kwargs["patrons_rewards"] = str(patrons_rewards)
+        if leftover is not None:
+            factory_kwargs["leftover"] = str(leftover)
+        if withdrawals_merkle_root is not None:
+            factory_kwargs["withdrawals_merkle_root"] = withdrawals_merkle_root
+        if total_withdrawals is not None:
+            factory_kwargs["total_withdrawals"] = str(total_withdrawals)
+
+        snapshot = await FinalizedEpochSnapshotFactory.create(**factory_kwargs)
+        return snapshot
diff --git a/backend/tests/v2/factories/patrons.py b/backend/tests/v2/factories/patrons.py
new file mode 100644
index 0000000000..a9cac8dc64
--- /dev/null
+++ b/backend/tests/v2/factories/patrons.py
@@ -0,0 +1,52 @@
+from datetime import datetime, timezone
+from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
+
+from app.infrastructure.database.models import PatronModeEvent, User
+from tests.v2.factories.base import FactorySetBase
+from tests.v2.factories.users import UserFactorySet
+from v2.core.types import Address
+
+
+class PatronModeEventFactory(AsyncSQLAlchemyFactory):
+    class Meta:
+        model = PatronModeEvent
+        sqlalchemy_session_persistence = "commit"
+
+    user_address = None
+    patron_mode_enabled = True
+    created_at = datetime.now(timezone.utc)
+
+
+class PatronModeEventFactorySet(FactorySetBase):
+    _factories = {"patron_event": PatronModeEventFactory}
+
+    async def create(
+        self,
+        user: User | Address,
+        patron_mode_enabled: bool = True,
+        created_at: datetime | None = None,
+    ) -> PatronModeEvent:
+        """
+        Create a patron mode event.
+
+        Args:
+            user: The user or user address to create the event for
+            patron_mode_enabled: Whether patron mode is enabled (default: True)
+            created_at: Optional timestamp for when the event was created (default: now)
+
+        Returns:
+            The created patron mode event
+        """
+        if not isinstance(user, User):
+            user = await UserFactorySet(self.session).get_or_create(user)
+
+        factory_kwargs = {
+            "user_address": user.address,
+            "patron_mode_enabled": patron_mode_enabled,
+        }
+
+        if created_at is not None:
+            factory_kwargs["created_at"] = created_at
+
+        event = await PatronModeEventFactory.create(**factory_kwargs)
+        return event
diff --git a/backend/tests/v2/factories/pending_snapshots.py b/backend/tests/v2/factories/pending_snapshots.py
new file mode 100644
index 0000000000..5baa787ddd
--- /dev/null
+++ b/backend/tests/v2/factories/pending_snapshots.py
@@ -0,0 +1,86 @@
+import random
+from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
+from factory import LazyAttribute
+
+from app.infrastructure.database.models import PendingEpochSnapshot
+from tests.v2.factories.base import FactorySetBase
+from v2.core.types import BigInteger
+
+
+class PendingEpochSnapshotFactory(AsyncSQLAlchemyFactory):
+    class Meta:
+        model = PendingEpochSnapshot
+        sqlalchemy_session_persistence = "commit"
+
+    epoch = None
+    eth_proceeds = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
+    total_effective_deposit = LazyAttribute(
+        lambda _: str(random.randint(1, 1000) * 10**18)
+    )
+    locked_ratio = LazyAttribute(lambda _: str(random.randint(1, 100)))
+    total_rewards = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
+    vanilla_individual_rewards = LazyAttribute(
+        lambda _: str(random.randint(1, 1000) * 10**18)
+    )
+    operational_cost = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))
+    ppf = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))
+    community_fund = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))
+
+
+class PendingEpochSnapshotFactorySet(FactorySetBase):
+    _factories = {"pending_snapshot": PendingEpochSnapshotFactory}
+
+    async def create(
+        self,
+        epoch: int,
+        eth_proceeds: BigInteger | None = None,
+        total_effective_deposit: BigInteger | None = None,
+        locked_ratio: BigInteger | None = None,
+        total_rewards: BigInteger | None = None,
+        vanilla_individual_rewards: BigInteger | None = None,
+        operational_cost: BigInteger | None = None,
+        ppf: BigInteger | None = None,
+        community_fund: BigInteger | None = None,
+    ) -> PendingEpochSnapshot:
+        """
+        Create a pending epoch snapshot.
+
+        Args:
+            epoch: The epoch number
+            eth_proceeds: Optional ETH proceeds amount
+            total_effective_deposit: Optional total effective deposit amount
+            locked_ratio: Optional locked ratio
+            total_rewards: Optional total rewards amount
+            vanilla_individual_rewards: Optional vanilla individual rewards amount
+            operational_cost: Optional operational cost amount
+            ppf: Optional PPF amount
+            community_fund: Optional community fund amount
+
+        Returns:
+            The created pending epoch snapshot
+        """
+        factory_kwargs = {
+            "epoch": epoch,
+        }
+
+        if eth_proceeds is not None:
+            factory_kwargs["eth_proceeds"] = str(eth_proceeds)
+        if total_effective_deposit is not None:
+            factory_kwargs["total_effective_deposit"] = str(total_effective_deposit)
+        if locked_ratio is not None:
+            factory_kwargs["locked_ratio"] = str(locked_ratio)
+        if total_rewards is not None:
+            factory_kwargs["total_rewards"] = str(total_rewards)
+        if vanilla_individual_rewards is not None:
+            factory_kwargs["vanilla_individual_rewards"] = str(
+                vanilla_individual_rewards
+            )
+        if operational_cost is not None:
+            factory_kwargs["operational_cost"] = str(operational_cost)
+        if ppf is not None:
+            factory_kwargs["ppf"] = str(ppf)
+        if community_fund is not None:
+            factory_kwargs["community_fund"] = str(community_fund)
+
+        snapshot = await PendingEpochSnapshotFactory.create(**factory_kwargs)
+        return snapshot
diff --git a/backend/tests/v2/factories/users.py b/backend/tests/v2/factories/users.py
index 5f0b960b53..b12fd95a5c 100644
--- a/backend/tests/v2/factories/users.py
+++ b/backend/tests/v2/factories/users.py
@@ -20,7 +20,7 @@ class Meta:
 class UserFactorySet(FactorySetBase):
     _factories = {"user": UserFactory}
 
-    async def create(self, address: Address) -> User:
+    async def create(self, address: Address | None = None) -> User:
         factory_kwargs = {}
 
         if address is not None:
diff --git a/backend/tests/v2/fake_subgraphs/epochs.py b/backend/tests/v2/fake_subgraphs/epochs.py
index 758a50943d..71c47c6239 100644
--- a/backend/tests/v2/fake_subgraphs/epochs.py
+++ b/backend/tests/v2/fake_subgraphs/epochs.py
@@ -1,5 +1,7 @@
+from pydantic import TypeAdapter
 from app import exceptions
 from app.context.epoch.details import EpochDetails
+from v2.epochs.subgraphs import EpochSubgraphItem
 from tests.v2.fake_subgraphs.helpers import FakeEpochEventDetails
 from v2.core.exceptions import EpochsNotFound
 
@@ -13,6 +15,19 @@ def __init__(self, epochs_events: list[FakeEpochEventDetails] | None = None):
             lambda epoch_event: epoch_event.to_dict(), epochs_events
         )
 
+    async def fetch_epoch_by_number(self, epoch_number: int) -> EpochSubgraphItem:
+        """
+        Simulate fetching epoch details by epoch number.
+        """
+        matching_epochs = [
+            epoch for epoch in self.epochs_events if epoch["epoch"] == epoch_number
+        ]
+
+        if not matching_epochs:
+            raise exceptions.EpochNotIndexed(epoch_number)
+
+        return TypeAdapter(EpochSubgraphItem).validate_python(matching_epochs[0])
+
     async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
         """
         Simulate fetching epoch details by epoch number.
diff --git a/backend/tests/v2/project_rewards/__init__.py b/backend/tests/v2/project_rewards/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/tests/v2/project_rewards/conftest.py b/backend/tests/v2/project_rewards/conftest.py
new file mode 100644
index 0000000000..651063bb62
--- /dev/null
+++ b/backend/tests/v2/project_rewards/conftest.py
@@ -0,0 +1,2 @@
+from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory  # noqa: F401
+from tests.v2.fake_subgraphs.conftest import fake_epochs_subgraph_factory  # noqa: F401
diff --git a/backend/tests/v2/project_rewards/test_estimated_budged.py b/backend/tests/v2/project_rewards/test_estimated_budged.py
new file mode 100644
index 0000000000..04af41db6e
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_estimated_budged.py
@@ -0,0 +1,187 @@
+import pytest
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from v2.glms.dependencies import get_glm_balance_of_deposits
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
+from tests.v2.fake_contracts.conftest import FakeEpochsContractCallable
+from fastapi import FastAPI
+
+
+@pytest.mark.asyncio
+async def test_estimates_budget_for_single_epoch(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_app: FastAPI,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should correctly estimate budget for a single epoch deposit
+    We are testing this while in finalized epoch.
+    """
+    # Given
+    glm_amount = 1000 * 10**18  # 1000 GLM
+    epoch_duration = 90 * 24 * 60 * 60  # 90 days in seconds
+    epoch_start = 1000
+    finalized_epoch = 1
+    glm_balance = 5000 * 10**18  # 5000 GLM total in contract
+
+    # Mock contracts
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            finalized_epoch=finalized_epoch,
+            current_epoch=finalized_epoch + 1,
+            pending_epoch=None,
+            future_epoch_props=(0, 0, epoch_start, epoch_duration),
+        )
+    )
+
+    # Create pending epoch snapshot
+    await factories.pending_snapshots.create(epoch=finalized_epoch)
+
+    # finalize epoch snapshot
+    await factories.finalized_snapshots.create(
+        epoch=finalized_epoch,
+    )
+
+    # Mock GLM balance
+    fast_app.dependency_overrides[get_glm_balance_of_deposits] = lambda: glm_balance
+
+    # Mock matched rewards estimator
+    # rewards_estimator_mock = MagicMock(spec=MatchedRewardsEstimator)
+    # rewards_estimator_mock.get.return_value = leverage
+    # fast_app.dependency_overrides[get_matched_rewards_estimator] = lambda: rewards_estimator_mock
+
+    request_data = {"glm_amount": str(glm_amount), "number_of_epochs": 1}
+
+    async with fast_client as client:
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+
+        assert resp.status_code == 200
+        response = resp.json()
+        assert response["budget"] != "0"
+        assert response["matchedFunding"] == "0"
+
+
+@pytest.mark.asyncio
+async def test_estimates_budget_for_multiple_epochs(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_app: FastAPI,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should correctly estimate budget for multiple epochs"""
+    # Given
+    glm_amount = 1000 * 10**18
+    number_of_epochs = 1
+    epoch_duration = 90 * 24 * 60 * 60
+    epoch_start = 1000
+    finalized_epoch = 1
+    glm_balance = 5000 * 10**18
+
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+    alice_allocation = await factories.allocations.create(
+        user=alice,
+        epoch=finalized_epoch,
+    )
+    bob_allocation = await factories.allocations.create(
+        user=bob,
+        epoch=finalized_epoch,
+    )
+
+    # Create pending epoch snapshot
+    await factories.pending_snapshots.create(epoch=finalized_epoch)
+
+    # finalize epoch snapshot
+    finalized_snapshot = await factories.finalized_snapshots.create(
+        epoch=finalized_epoch,
+    )
+
+    # Mock contracts
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            finalized_epoch=finalized_epoch,
+            future_epoch_props=(0, 0, epoch_start, epoch_duration),
+        )
+    )
+
+    # Mock dependencies
+    fast_app.dependency_overrides[get_glm_balance_of_deposits] = lambda: glm_balance
+
+    # Mock matched rewards estimator
+    # rewards_estimator_mock = MagicMock(spec=GetMatchedRewardsEstimatorInAW)
+    # rewards_estimator_mock.get.return_value = leverage
+    # fast_app.dependency_overrides[GetMatchedRewardsEstimatorInAW] = lambda: rewards_estimator_mock
+
+    request_data = {"glm_amount": str(glm_amount), "number_of_epochs": number_of_epochs}
+
+    async with fast_client as client:
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+
+        assert resp.status_code == 200
+        result = resp.json()
+
+        assert result["budget"] != "0"
+        assert result["matchedFunding"] != "0"
+        leverage = int(finalized_snapshot.matched_rewards) / (
+            int(alice_allocation.amount) + int(bob_allocation.amount)
+        )
+        assert int(result["matchedFunding"]) == int(int(result["budget"]) * leverage)
+
+        one_epoch_budget = int(result["budget"])
+        one_epoch_matched_funding = int(result["matchedFunding"])
+
+        # Test two epochs
+        request_data = {"glm_amount": str(glm_amount), "number_of_epochs": 2}
+
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+
+        assert resp.status_code == 200
+        result = resp.json()
+        assert int(result["budget"]) != "0"
+        assert int(result["matchedFunding"]) != "0"
+
+        assert int(result["budget"]) == one_epoch_budget * 2
+        assert int(result["matchedFunding"]) == one_epoch_matched_funding * 2
+
+        # Test zero GLM amount
+        request_data = {"glm_amount": str(0), "number_of_epochs": 1}
+
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+
+        assert resp.status_code == 200
+        result = resp.json()
+        assert int(result["budget"]) == 0
+        assert int(result["matchedFunding"]) == 0
+
+
+@pytest.mark.asyncio
+async def test_validates_request_parameters(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+):
+    """Should validate request parameters"""
+    async with fast_client as client:
+        # Test negative GLM amount
+        resp = await client.post(
+            "/rewards/estimated_budget",
+            json={"glm_amount": "-1000", "number_of_epochs": 1},
+        )
+        assert resp.status_code == 422
+
+        # Test zero epochs
+        resp = await client.post(
+            "/rewards/estimated_budget",
+            json={"glm_amount": "1000", "number_of_epochs": 0},
+        )
+        assert resp.status_code == 422
+
+        # Test negative epochs
+        resp = await client.post(
+            "/rewards/estimated_budget",
+            json={"glm_amount": "1000", "number_of_epochs": -1},
+        )
+        assert resp.status_code == 422
diff --git a/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py b/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
new file mode 100644
index 0000000000..a76122f602
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
@@ -0,0 +1,66 @@
+import pytest
+from fastapi import status
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from tests.v2.factories import FactoriesAggregator
+
+"""Test cases for the GET /rewards/budget/{user_address}/epoch/{epoch_number} endpoint"""
+
+
+@pytest.mark.asyncio
+async def test_returns_none_when_no_budget(
+    fast_client: AsyncClient, fast_session: AsyncSession, factories: FactoriesAggregator
+):
+    """Should return 204 No Content when user has no budget for the epoch"""
+    # Given: a user with no budget
+    alice = await factories.users.get_or_create_alice()
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/1")
+        assert resp.status_code == status.HTTP_204_NO_CONTENT
+        assert resp.text == ""
+
+
+@pytest.mark.asyncio
+async def test_returns_budget_when_it_exists(
+    fast_client: AsyncClient, factories: FactoriesAggregator
+):
+    """Should return user's budget when it exists for the epoch"""
+
+    # Given: users with budgets for different epochs
+    alice = await factories.users.get_or_create_alice()
+    budget1 = await factories.budgets.create(user=alice, epoch=1)
+    budget2 = await factories.budgets.create(user=alice, epoch=2)
+
+    async with fast_client as client:
+        # Budget for epoch 1
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/1")
+        assert resp.status_code == status.HTTP_200_OK
+        assert resp.json() == {"budget": budget1.budget}
+
+        # Budget for epoch 2
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/2")
+        assert resp.status_code == status.HTTP_200_OK
+        assert resp.json() == {"budget": budget2.budget}
+
+        # No budget for epoch 3
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/3")
+        assert resp.status_code == status.HTTP_204_NO_CONTENT
+        assert resp.text == ""
+
+
+@pytest.mark.asyncio
+async def test_returns_zero_budget_when_amount_is_zero(
+    fast_client: AsyncClient, factories: FactoriesAggregator
+):
+    """Should return budget even when amount is zero"""
+
+    # Given: a user with zero budget
+    alice = await factories.users.get_or_create_alice()
+    await factories.budgets.create(user=alice, epoch=1, amount=0)
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/1")
+        assert resp.status_code == status.HTTP_200_OK
+        assert resp.json() == {"budget": "0"}
diff --git a/backend/tests/v2/project_rewards/test_get_epoch_budgets.py b/backend/tests/v2/project_rewards/test_get_epoch_budgets.py
new file mode 100644
index 0000000000..9c73c6f9c1
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_epoch_budgets.py
@@ -0,0 +1,114 @@
+import pytest
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+from tests.v2.factories import FactoriesAggregator
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_budgets_when_no_users(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+):
+    """Should return empty budgets list when no users have budgets for the epoch"""
+
+    async with fast_client as client:
+        resp = await client.get("/rewards/budgets/epoch/1")
+        assert resp.status_code == 200
+        assert resp.json() == {"budgets": []}
+
+
+@pytest.mark.asyncio
+async def test_returns_all_user_budgets_for_epoch(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return budgets for all users in the given epoch"""
+
+    # Given: multiple users with budgets in different epochs
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+    charlie = await factories.users.get_or_create_charlie()
+
+    # Create budgets for different epochs
+    alice_budget1 = await factories.budgets.create(user=alice, epoch=1, amount=100)
+    bob_budget1 = await factories.budgets.create(user=bob, epoch=1, amount=200)
+    charlie_budget1 = await factories.budgets.create(user=charlie, epoch=1, amount=300)
+
+    # Create budgets for a different epoch
+    await factories.budgets.create(user=alice, epoch=2, amount=400)
+    await factories.budgets.create(user=bob, epoch=2, amount=500)
+
+    async with fast_client as client:
+        # When: requesting budgets for epoch 1
+        resp = await client.get("/rewards/budgets/epoch/1")
+
+        # Then: should return all budgets for epoch 1
+        assert resp.status_code == 200
+        budgets = resp.json()["budgets"]
+
+        # Convert to set for order-independent comparison
+        expected_budgets = [
+            {"address": alice.address, "amount": str(alice_budget1.budget)},
+            {"address": bob.address, "amount": str(bob_budget1.budget)},
+            {"address": charlie.address, "amount": str(charlie_budget1.budget)},
+        ]
+        assert len(budgets) == 3
+        assert {(b["address"], b["amount"]) for b in budgets} == {
+            (b["address"], b["amount"]) for b in expected_budgets
+        }
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_budgets_for_future_epoch(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return empty budgets list for an epoch with no budgets"""
+
+    # Given: users with budgets in epoch 1
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+
+    await factories.budgets.create(user=alice, epoch=1, amount=100)
+    await factories.budgets.create(user=bob, epoch=1, amount=200)
+
+    async with fast_client as client:
+        # When: requesting budgets for epoch 2 (which has no budgets)
+        resp = await client.get("/rewards/budgets/epoch/2")
+
+        # Then: should return empty list
+        assert resp.status_code == 200
+        assert resp.json() == {"budgets": []}
+
+
+@pytest.mark.asyncio
+async def test_includes_zero_budgets(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should include users with zero budgets in the response"""
+
+    # Given: users with zero and non-zero budgets
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+
+    alice_budget = await factories.budgets.create(user=alice, epoch=1, amount=100)
+    await factories.budgets.create(user=bob, epoch=1, amount=0)
+
+    async with fast_client as client:
+        resp = await client.get("/rewards/budgets/epoch/1")
+
+        assert resp.status_code == 200
+        budgets = resp.json()["budgets"]
+
+        expected_budgets = [
+            {"address": alice.address, "amount": str(alice_budget.budget)},
+            {"address": bob.address, "amount": "0"},
+        ]
+        assert len(budgets) == 2
+        assert {(b["address"], b["amount"]) for b in budgets} == {
+            (b["address"], b["amount"]) for b in expected_budgets
+        }
diff --git a/backend/tests/v2/project_rewards/test_get_rewards_leverage.py b/backend/tests/v2/project_rewards/test_get_rewards_leverage.py
new file mode 100644
index 0000000000..c22824f20b
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_rewards_leverage.py
@@ -0,0 +1,193 @@
+from fastapi import FastAPI
+import pytest
+from http import HTTPStatus
+from unittest.mock import MagicMock
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.exceptions import InvalidEpoch, NotImplementedForGivenEpochState
+from v2.matched_rewards.services import MatchedRewardsEstimator
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
+from tests.v2.fake_contracts.conftest import FakeEpochsContractCallable
+from v2.matched_rewards.dependencies import get_matched_rewards_estimator
+
+
+@pytest.mark.asyncio
+async def test_returns_leverage_for_finalized_epoch(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return leverage based on finalized snapshot for finalized epoch"""
+    # Given: a finalized epoch with snapshot and allocations
+    epoch_number = 1
+
+    # Mock contracts to return finalized state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=epoch_number + 1,  # Current epoch is ahead
+            finalized_epoch=epoch_number,
+            pending_epoch=None,
+        )
+    )
+
+    # Create pending snapshot
+    await factories.pending_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create finalized snapshot
+    finalized_snapshot = await factories.finalized_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create some allocations
+    alice = await factories.users.get_or_create_alice()
+    alice_allocation = await factories.allocations.create(
+        user=alice,
+        epoch=epoch_number,
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        resp_leverage = int(resp.json()["leverage"])
+        expected_leverage = int(
+            int(finalized_snapshot.matched_rewards) / int(alice_allocation.amount)
+        )
+        assert resp_leverage == expected_leverage
+
+
+@pytest.mark.asyncio
+async def test_returns_leverage_for_pending_epoch(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+    fast_app: FastAPI,
+):
+    """Should return leverage based on estimated rewards for pending epoch"""
+    # Given: a pending epoch with allocations
+    epoch_number = 1
+    estimated_rewards = 100_000_000
+
+    # Mock contracts to return pending state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=epoch_number + 1,  # Current epoch is ahead
+            finalized_epoch=epoch_number,
+            pending_epoch=None,
+        )
+    )
+
+    # Create pending snapshot
+    await factories.pending_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create some allocations
+    alice = await factories.users.get_or_create_alice()
+    alice_allocation = await factories.allocations.create(
+        user=alice,
+        epoch=epoch_number,
+    )
+
+    # Mock rewards estimator
+    rewards_estimator = MagicMock(spec=MatchedRewardsEstimator)
+    rewards_estimator.get.return_value = estimated_rewards
+    fast_app.dependency_overrides[
+        get_matched_rewards_estimator
+    ] = lambda: rewards_estimator
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {"leverage": estimated_rewards / alice_allocation.amount}
+
+
+@pytest.mark.asyncio
+async def test_returns_zero_leverage_when_no_allocations(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return zero leverage when there are no allocations"""
+    # Given: a finalized epoch with snapshot but no allocations
+    epoch_number = 1
+    matched_rewards = 1000 * 10**18
+
+    # Mock contracts to return finalized state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=2,
+            finalized_epoch=epoch_number,
+        )
+    )
+
+    # Create pending snapshot
+    await factories.pending_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create finalized snapshot
+    await factories.finalized_snapshots.create(
+        epoch=epoch_number,
+        matched_rewards=matched_rewards,
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {"leverage": 0}
+
+
+@pytest.mark.asyncio
+async def test_raises_error_when_snapshot_missing(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+):
+    """Should raise MissingSnapshot when finalized snapshot is missing"""
+    # Given: a finalized epoch without snapshot
+    epoch_number = 1
+
+    # Mock contracts to return finalized state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=2,
+            finalized_epoch=epoch_number,
+        )
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.BAD_REQUEST
+        assert resp.json() == {"message": InvalidEpoch.description}
+
+
+@pytest.mark.asyncio
+async def test_raises_error_for_invalid_epoch_state(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+):
+    """Should raise NotImplementedForGivenEpochState for invalid epoch state"""
+    # Given: an epoch in invalid state (after PENDING)
+    epoch_number = 1
+
+    # Mock contracts to return future epoch
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=1,
+            finalized_epoch=0,
+            future_epoch_props=(0, 0, 2000, 1000),  # Future epoch
+        )
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.BAD_REQUEST
+        assert resp.json() == {"message": NotImplementedForGivenEpochState.description}
diff --git a/backend/tests/v2/project_rewards/test_get_unused_rewards.py b/backend/tests/v2/project_rewards/test_get_unused_rewards.py
new file mode 100644
index 0000000000..439c145c9d
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_unused_rewards.py
@@ -0,0 +1,142 @@
+import pytest
+from http import HTTPStatus
+from datetime import datetime, timezone
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_subgraphs.conftest import FakeEpochsSubgraphCallable
+from tests.v2.fake_subgraphs.helpers import FakeEpochEventDetails
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_when_no_budgets(
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return empty list and zero value when there are no budgets"""
+    # Given: an epoch with no budgets
+    epoch_number = 1
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=epoch_number,
+            )
+        ]
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/unused/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {
+            "addresses": [],
+            "value": "0",
+        }
+
+
+@pytest.mark.asyncio
+async def test_returns_unused_budgets_excluding_donors_and_patrons(
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return budgets excluding donors and patrons"""
+    # Given: an epoch with various users
+    epoch_number = 1
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=epoch_number,
+            )
+        ]
+    )
+
+    # Create users with different roles
+    alice = await factories.users.get_or_create_alice()  # Will be a donor
+    bob = await factories.users.get_or_create_bob()  # Will be a patron
+    charlie = await factories.users.get_or_create_charlie()  # Will have unused budget
+    dave = await factories.users.create()  # Will have unused budget
+
+    # Create budgets for all users
+    await factories.budgets.create(user=alice, epoch=epoch_number, amount=100)
+    await factories.budgets.create(user=bob, epoch=epoch_number, amount=200)
+    charlie_budget = await factories.budgets.create(
+        user=charlie, epoch=epoch_number, amount=300
+    )
+    dave_budget = await factories.budgets.create(
+        user=dave, epoch=epoch_number, amount=400
+    )
+
+    # Make Alice a donor by creating an allocation
+    await factories.allocations.create(user=alice, epoch=epoch_number)
+
+    # Make Bob a patron
+    await factories.patrons.create(
+        user=bob,
+        patron_mode_enabled=True,
+        created_at=datetime.fromtimestamp(1500, timezone.utc),
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/unused/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+
+        result = resp.json()
+        assert sorted(result["addresses"]) == sorted([charlie.address, dave.address])
+        assert result["value"] == str(
+            int(charlie_budget.budget) + int(dave_budget.budget)
+        )
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_when_all_users_are_donors_or_patrons(
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return empty when all users with budgets are either donors or patrons"""
+    # Given: an epoch where all users are donors or patrons
+    epoch_number = 1
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=epoch_number,
+            )
+        ]
+    )
+
+    # Create users
+    alice = await factories.users.get_or_create_alice()  # Will be a donor
+    bob = await factories.users.get_or_create_bob()  # Will be a patron
+
+    # Create budgets
+    await factories.budgets.create(user=alice, epoch=epoch_number)
+    await factories.budgets.create(user=bob, epoch=epoch_number)
+
+    # Make Alice a donor
+    await factories.allocations.create(user=alice, epoch=epoch_number)
+
+    # Make Bob a patron
+    await factories.patrons.create(
+        user=bob,
+        patron_mode_enabled=True,
+        created_at=datetime.fromtimestamp(1500, timezone.utc),
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/unused/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {
+            "addresses": [],
+            "value": "0",
+        }
diff --git a/backend/tests/v2/project_rewards/test_get_upcoming_budget.py b/backend/tests/v2/project_rewards/test_get_upcoming_budget.py
new file mode 100644
index 0000000000..eb9febe031
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_upcoming_budget.py
@@ -0,0 +1,114 @@
+from unittest.mock import MagicMock
+import pytest
+from http import HTTPStatus
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.engine.user.effective_deposit import DepositEvent, DepositSource, EventType
+from v2.deposits.dependencies import (
+    GetDepositEventsRepository,
+    get_deposit_events_repository,
+)
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
+from tests.v2.fake_contracts.conftest import FakeEpochsContractCallable
+from tests.v2.fake_subgraphs.conftest import FakeEpochsSubgraphCallable
+from tests.v2.fake_subgraphs.helpers import FakeEpochEventDetails
+from fastapi import FastAPI
+from v2.core.dependencies import get_current_timestamp
+
+
+@pytest.mark.asyncio
+async def test_returns_upcoming_budget_with_no_deposits(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_app: FastAPI,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return zero budget when user has no deposits"""
+    # Given: a user and mocked epoch data
+    alice = await factories.users.get_or_create_alice()
+    current_epoch = 1
+    epoch_start = 0
+
+    # Mock contracts and subgraph
+    fake_epochs_contract_factory(FakeEpochsContractDetails(current_epoch=current_epoch))
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=current_epoch,
+                from_ts=epoch_start,
+                to_ts=2000,
+            )
+        ]
+    )
+
+    events_repository = MagicMock(spec=GetDepositEventsRepository)
+    events_repository.get_all_users_events.return_value = {}
+    fast_app.dependency_overrides[
+        get_deposit_events_repository
+    ] = lambda: events_repository
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/upcoming")
+        assert resp.json() == {"upcomingBudget": "0"}
+        assert resp.status_code == HTTPStatus.OK
+
+
+@pytest.mark.asyncio
+async def test_returns_upcoming_budget_with_deposits(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+    fast_app: FastAPI,
+):
+    """Should calculate and return upcoming budget based on user's deposits"""
+    # Given: a user and mocked epoch data
+    alice = await factories.users.get_or_create_alice()
+    current_epoch = 1
+    current_timestamp = 1000
+    epoch_start = 0
+    deposit_amount = 100 * 10**18  # 100 tokens
+
+    # Mock contracts and subgraph
+    fake_epochs_contract_factory(FakeEpochsContractDetails(current_epoch=current_epoch))
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=current_epoch,
+                from_ts=epoch_start,
+                to_ts=2000,
+            )
+        ]
+    )
+
+    events_repository = MagicMock(spec=GetDepositEventsRepository)
+    events_repository.get_all_users_events.return_value = {
+        alice.address: [
+            DepositEvent(
+                user=alice.address,
+                type=EventType.LOCK,
+                timestamp=epoch_start + 100,
+                amount=deposit_amount,
+                deposit_before=0,
+                source=DepositSource.OCTANT,
+            )
+        ]
+    }
+    fast_app.dependency_overrides[
+        get_deposit_events_repository
+    ] = lambda: events_repository
+    fast_app.dependency_overrides[get_current_timestamp] = lambda: current_timestamp
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/upcoming")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() != {"upcomingBudget": "0"}
diff --git a/backend/v2/allocations/dependencies.py b/backend/v2/allocations/dependencies.py
index df1ea54741..d2aca5e4ba 100644
--- a/backend/v2/allocations/dependencies.py
+++ b/backend/v2/allocations/dependencies.py
@@ -5,7 +5,7 @@
 from v2.allocations.validators import SignatureVerifier
 from v2.core.dependencies import GetChainSettings, GetSession
 from v2.epochs.dependencies import GetEpochsSubgraph, GetOpenAllocationWindowEpochNumber
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimatorInAW
 from v2.projects.dependencies import GetProjectsContracts
 from v2.uniqueness_quotients.dependencies import GetUQScoreGetter
 
@@ -30,7 +30,7 @@ async def get_allocator(
     signature_verifier: GetSignatureVerifier,
     uq_score_getter: GetUQScoreGetter,
     projects_contracts: GetProjectsContracts,
-    matched_rewards_estimator: GetMatchedRewardsEstimator,
+    matched_rewards_estimator: GetMatchedRewardsEstimatorInAW,
 ) -> Allocator:
     return Allocator(
         session,
diff --git a/backend/v2/allocations/router.py b/backend/v2/allocations/router.py
index 58480c6f27..af07955cb5 100644
--- a/backend/v2/allocations/router.py
+++ b/backend/v2/allocations/router.py
@@ -1,5 +1,10 @@
 from typing import Annotated
 from fastapi import APIRouter, Query
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.projects.dependencies import GetProjectsContracts
+from v2.uniqueness_quotients.dependencies import GetUQScoreGetter
+from v2.allocations.services import simulate_allocation
+from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
 from v2.allocations.repositories import (
     get_all_allocations_for_epoch,
     get_allocations_by_user,
@@ -15,6 +20,8 @@
     EpochAllocationsResponseV1,
     EpochDonorsResponseV1,
     ProjectAllocationV1,
+    SimulateAllocationPayloadV1,
+    SimulateAllocationResponseV1,
     UserAllocationNonceV1,
     UserAllocationRequest,
     UserAllocationRequestV1,
@@ -83,15 +90,29 @@ async def get_all_allocations_for_epoch_v1(
     return EpochAllocationsResponseV1(allocations=donations)
 
 
-# @api.post("/leverage/{user_address}")
-# async def simulate_allocation_v1(
-#     user_address: Address, payload: SimulateAllocationPayloadV1
-# ) -> None:
-#     """
-#     Simulates an allocation and get the expected leverage, threshold and matched rewards.
-#     """
-#     # TODO: implement
-#     pass
+@api.post("/leverage/{user_address}")
+async def simulate_allocation_v1(
+    session: GetSession,
+    projects_contracts: GetProjectsContracts,
+    matched_rewards_estimator: GetMatchedRewardsEstimator,
+    uq_score_getter: GetUQScoreGetter,
+    pending_epoch_number: GetOpenAllocationWindowEpochNumber,
+    # Request Parameters
+    user_address: Address,
+    payload: SimulateAllocationPayloadV1,
+) -> SimulateAllocationResponseV1:
+    """
+    Simulates an allocation and get the expected leverage, threshold and matched rewards.
+    """
+    return await simulate_allocation(
+        session,
+        projects_contracts,
+        matched_rewards_estimator,
+        uq_score_getter,
+        pending_epoch_number,
+        user_address,
+        payload.allocations,
+    )
 
 
 @api.get("/project/{project_address}/epoch/{epoch_number}")
diff --git a/backend/v2/allocations/schemas.py b/backend/v2/allocations/schemas.py
index 633059b196..912a7b84c7 100644
--- a/backend/v2/allocations/schemas.py
+++ b/backend/v2/allocations/schemas.py
@@ -66,16 +66,16 @@ class UserAllocationsResponseV1(OctantModel):
     is_manually_edited: bool | None
 
 
-class SimulateAllocationPayloadV1(OctantModel):
-    allocations: list[AllocationRequestV1]
-
-
 class ProjectMatchedRewardsV1(OctantModel):
     address: Address
     value: BigInteger
 
 
+class SimulateAllocationPayloadV1(OctantModel):
+    allocations: list[AllocationRequestV1]
+
+
 class SimulateAllocationResponseV1(OctantModel):
     leverage: Decimal
-    threshold: int
+    threshold: BigInteger | None
     matched: list[ProjectMatchedRewardsV1]
diff --git a/backend/v2/allocations/services.py b/backend/v2/allocations/services.py
index a5b15dc9ef..f5117fe1bb 100644
--- a/backend/v2/allocations/services.py
+++ b/backend/v2/allocations/services.py
@@ -2,15 +2,27 @@
 
 from app import exceptions
 from sqlalchemy.ext.asyncio import AsyncSession
+from v2.core.types import Address
 from v2.allocations.repositories import (
     get_allocations_with_user_uqs,
     soft_delete_user_allocations_by_epoch,
     store_allocation_request,
 )
-from v2.allocations.schemas import AllocationWithUserUQScore, UserAllocationRequest
+from v2.allocations.schemas import (
+    AllocationRequestV1,
+    AllocationWithUserUQScore,
+    ProjectMatchedRewardsV1,
+    SimulateAllocationResponseV1,
+    UserAllocationRequest,
+)
 from v2.allocations.validators import SignatureVerifier
 from v2.matched_rewards.services import MatchedRewardsEstimator
-from v2.project_rewards.capped_quadriatic import cqf_simulate_leverage
+from v2.project_rewards.capped_quadratic import (
+    MR_FUNDING_CAP_PERCENT,
+    capped_quadratic_funding,
+    cqf_calculate_individual_leverage,
+    cqf_simulate_leverage,
+)
 from v2.projects.contracts import ProjectsContracts
 from v2.uniqueness_quotients.dependencies import UQScoreGetter
 from v2.users.repositories import get_user_by_address
@@ -112,6 +124,85 @@ async def allocate(
     return request.user_address
 
 
+async def simulate_allocation(
+    # Component dependencies
+    session: AsyncSession,
+    projects_contracts: ProjectsContracts,
+    matched_rewards_estimator: MatchedRewardsEstimator,
+    uq_score_getter: UQScoreGetter,
+    # Arguments
+    epoch_number: int,
+    user_address: Address,
+    new_allocations: list[AllocationRequestV1],
+) -> SimulateAllocationResponseV1:
+    """
+    Simulate the allocation of the user and calculate the leverage and matched project rewards.
+    This just "ignores" the user's current allocations and calculates the leverage as if the user
+    had made the allocation.
+    """
+
+    # Get or calculate UQ score of the user
+    user_uq_score = await uq_score_getter.get_or_calculate(
+        epoch_number=epoch_number,
+        user_address=user_address,
+    )
+    new_allocations_with_uq = [
+        AllocationWithUserUQScore(
+            project_address=a.project_address,
+            amount=a.amount,
+            user_address=user_address,
+            user_uq_score=user_uq_score,
+        )
+        for a in new_allocations
+    ]
+
+    # Calculate leverage and matched project rewards
+    all_projects = await projects_contracts.get_project_addresses(epoch_number)
+    existing_allocations = await get_allocations_with_user_uqs(session, epoch_number)
+    matched_rewards = await matched_rewards_estimator.get()
+
+    allocations_without_user = [
+        a for a in existing_allocations if a.user_address != user_address
+    ]
+
+    # Calculate capped quadratic funding before and after the user's allocation
+    # Leverage is more or less a difference between the two (before - after)
+    before_allocation = capped_quadratic_funding(
+        allocations_without_user,
+        matched_rewards,
+        all_projects,
+        MR_FUNDING_CAP_PERCENT,
+    )
+    after_allocation = capped_quadratic_funding(
+        allocations_without_user + new_allocations_with_uq,
+        matched_rewards,
+        all_projects,
+        MR_FUNDING_CAP_PERCENT,
+    )
+
+    leverage = cqf_calculate_individual_leverage(
+        new_allocations_amount=sum(a.amount for a in new_allocations_with_uq),
+        project_addresses=[a.project_address for a in new_allocations_with_uq],
+        before_allocation=before_allocation,
+        after_allocation=after_allocation,
+    )
+
+    return SimulateAllocationResponseV1(
+        leverage=leverage,
+        threshold=None,
+        matched=sorted(
+            [
+                ProjectMatchedRewardsV1(
+                    address=p.address,
+                    value=p.matched,
+                )
+                for p in after_allocation.project_fundings.values()
+            ],
+            key=lambda x: x.address,
+        ),
+    )
+
+
 async def simulate_leverage(
     # Component dependencies
     session: AsyncSession,
diff --git a/backend/v2/allocations/socket.py b/backend/v2/allocations/socket.py
index fee236a31f..20dd00411e 100644
--- a/backend/v2/allocations/socket.py
+++ b/backend/v2/allocations/socket.py
@@ -26,7 +26,7 @@
     get_open_allocation_window_epoch_number,
 )
 from v2.matched_rewards.dependencies import (
-    get_matched_rewards_estimator,
+    get_matched_rewards_estimator_only_in_aw,
     get_matched_rewards_estimator_settings,
 )
 from v2.project_rewards.dependencies import get_project_rewards_estimator
@@ -74,7 +74,7 @@ async def create_dependencies_on_connect() -> AsyncGenerator[
                 projects_contracts,
                 get_projects_allocation_threshold_settings(),
             )
-            estimated_matched_rewards = await get_matched_rewards_estimator(
+            estimated_matched_rewards = await get_matched_rewards_estimator_only_in_aw(
                 epoch_number,
                 session,
                 epochs_subgraph,
@@ -131,7 +131,7 @@ async def create_dependencies_on_allocate() -> AsyncGenerator[
                 projects_contracts,
                 get_projects_allocation_threshold_settings(),
             )
-            estimated_matched_rewards = await get_matched_rewards_estimator(
+            estimated_matched_rewards = await get_matched_rewards_estimator_only_in_aw(
                 epoch_number,
                 session,
                 epochs_subgraph,
diff --git a/backend/v2/core/contracts.py b/backend/v2/core/contracts.py
index e73c01d134..39b66f5b85 100644
--- a/backend/v2/core/contracts.py
+++ b/backend/v2/core/contracts.py
@@ -8,4 +8,5 @@ class SmartContract:
     def __init__(self, w3: AsyncWeb3, abi: ABI, address: ChecksumAddress) -> None:
         self.abi = abi
         self.w3 = w3
+        self.address = address
         self.contract: AsyncContract = w3.eth.contract(address=address, abi=abi)
diff --git a/backend/v2/core/dependencies.py b/backend/v2/core/dependencies.py
index 4c238dcf0f..508138d5a0 100644
--- a/backend/v2/core/dependencies.py
+++ b/backend/v2/core/dependencies.py
@@ -1,4 +1,5 @@
 from functools import lru_cache
+import time
 from typing import Annotated, AsyncGenerator
 
 from fastapi import Depends
@@ -8,6 +9,9 @@
 from web3 import AsyncHTTPProvider, AsyncWeb3
 from web3.middleware import async_geth_poa_middleware
 
+from app.shared.blockchain_types import ChainTypes
+from v2.core.logic import compare_blockchain_types
+
 
 class OctantSettings(BaseSettings):
     model_config = SettingsConfigDict(env_file=".env", extra="ignore", frozen=True)
@@ -107,6 +111,12 @@ class ChainSettings(OctantSettings):
         description="The chain id to use for the signature verification.",
     )
 
+    is_mainnet: bool = Field(
+        default_factory=lambda: compare_blockchain_types(
+            Field(validation_alias="chain_id"), ChainTypes.MAINNET
+        )
+    )
+
 
 def get_chain_settings() -> ChainSettings:
     return ChainSettings()
@@ -127,7 +137,12 @@ def get_socketio_settings() -> SocketioSettings:
     return SocketioSettings()  # type: ignore[call-arg]
 
 
+def get_current_timestamp() -> int:
+    return int(time.time())
+
+
 GetSocketioSettings = Annotated[SocketioSettings, Depends(get_socketio_settings)]
 GetChainSettings = Annotated[ChainSettings, Depends(get_chain_settings)]
 Web3 = Annotated[AsyncWeb3, Depends(get_w3)]
 GetSession = Annotated[AsyncSession, Depends(get_db_session)]
+GetCurrentTimestamp = Annotated[int, Depends(get_current_timestamp)]
diff --git a/backend/v2/deposits/dependencies.py b/backend/v2/deposits/dependencies.py
index cbe9a36500..e1a350fcf6 100644
--- a/backend/v2/deposits/dependencies.py
+++ b/backend/v2/deposits/dependencies.py
@@ -1,7 +1,14 @@
 from typing import Annotated
 
 from fastapi import Depends
-from v2.core.dependencies import OctantSettings, Web3
+from app.constants import (
+    SABLIER_UNLOCK_GRACE_PERIOD_24_HRS,
+    TEST_SABLIER_UNLOCK_GRACE_PERIOD_15_MIN,
+)
+from v2.deposits.services import DepositEventsStore
+from v2.epochs.dependencies import GetEpochsSubgraph
+from v2.sablier.dependencies import GetSablierSubgraph
+from v2.core.dependencies import GetChainSettings, GetSession, OctantSettings, Web3
 from v2.deposits.contracts import DEPOSITS_ABI, DepositsContracts
 
 
@@ -17,3 +24,30 @@ def get_deposits_contracts(
     w3: Web3, settings: Annotated[DepositsSettings, Depends(get_deposits_settings)]
 ) -> DepositsContracts:
     return DepositsContracts(w3, DEPOSITS_ABI, settings.deposits_contract_address)  # type: ignore[arg-type]
+
+
+def get_deposit_events_repository(
+    session: GetSession,
+    epochs_subgraph: GetEpochsSubgraph,
+    sublier_subgraph: GetSablierSubgraph,
+    chain_settings: GetChainSettings,
+) -> DepositEventsStore:
+    sablier_unlock_grace_period = (
+        SABLIER_UNLOCK_GRACE_PERIOD_24_HRS
+        if chain_settings.is_mainnet
+        else TEST_SABLIER_UNLOCK_GRACE_PERIOD_15_MIN
+    )
+
+    return DepositEventsStore(
+        session,
+        epochs_subgraph,
+        sublier_subgraph,
+        sablier_unlock_grace_period,
+    )
+
+
+# Annotated dependencies
+GetDepositsContracts = Annotated[DepositsContracts, Depends(get_deposits_contracts)]
+GetDepositEventsRepository = Annotated[
+    DepositEventsStore, Depends(get_deposit_events_repository)
+]
diff --git a/backend/v2/deposits/repositories.py b/backend/v2/deposits/repositories.py
new file mode 100644
index 0000000000..b6d9083e64
--- /dev/null
+++ b/backend/v2/deposits/repositories.py
@@ -0,0 +1,22 @@
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import selectinload
+
+from app.infrastructure.database.models import Deposit
+from v2.core.types import Address
+from v2.core.transformers import transform_to_checksum_address
+
+
+async def get_all_deposit_events_for_epoch(
+    session: AsyncSession,
+    epoch_number: int,
+) -> dict[Address, Deposit]:
+    results = await session.scalars(
+        select(Deposit)
+        .options(selectinload(Deposit.user))
+        .where(Deposit.epoch == epoch_number)
+    )
+
+    return {
+        transform_to_checksum_address(result.user.address): result for result in results
+    }
diff --git a/backend/v2/deposits/services.py b/backend/v2/deposits/services.py
new file mode 100644
index 0000000000..4b37dd10e4
--- /dev/null
+++ b/backend/v2/deposits/services.py
@@ -0,0 +1,103 @@
+from itertools import groupby
+from operator import attrgetter
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.engine.user.effective_deposit import (
+    DepositEvent,
+    EventType,
+)
+from app.modules.common.sablier_events_mapper import (
+    FlattenStrategy,
+    flatten_sablier_events,
+    process_to_locks_and_unlocks,
+)
+from app.modules.user.events_generator.core import unify_deposit_balances
+from v2.deposits.repositories import get_all_deposit_events_for_epoch
+from v2.epochs.subgraphs import EpochsSubgraph
+from v2.sablier.subgraphs import SablierSubgraph
+
+
+class DepositEventsStore:
+    def __init__(
+        self,
+        session: AsyncSession,
+        epochs_subgraph: EpochsSubgraph,
+        sablier_subgraph: SablierSubgraph,
+        sablier_unlock_grace_period: int,
+    ):
+        self.session = session
+        self.epochs_subgraph = epochs_subgraph
+        self.sablier_subgraph = sablier_subgraph
+        self.sablier_unlock_grace_period = sablier_unlock_grace_period
+
+    async def get_all_users_events(
+        self,
+        epoch_number: int,
+        start_sec: int,
+        end_sec: int,
+    ) -> dict[str, list[DepositEvent]]:
+        """
+        Returns all user events (LOCK, UNLOCK) for a given epoch.
+        """
+
+        # Get all locked amounts for the previous epoch
+        epoch_start_locked_amounts = await get_all_deposit_events_for_epoch(
+            self.session, epoch_number - 1
+        )
+        epoch_start_events = [
+            DepositEvent(
+                user=user,
+                type=EventType.LOCK,
+                timestamp=start_sec,
+                amount=0,  # it is not a deposit in fact
+                deposit_before=int(deposit.epoch_end_deposit),
+            )
+            for user, deposit in epoch_start_locked_amounts.items()
+        ]
+
+        sablier_streams = await self.sablier_subgraph.get_all_streams_history()
+        mapped_streams = process_to_locks_and_unlocks(
+            sablier_streams, from_timestamp=start_sec, to_timestamp=end_sec
+        )
+        epoch_events = []
+        epoch_events += flatten_sablier_events(mapped_streams, FlattenStrategy.ALL)
+        epoch_events += await self.epochs_subgraph.fetch_locks_by_timestamp_range(
+            start_sec, end_sec
+        )
+        epoch_events += await self.epochs_subgraph.fetch_unlocks_by_timestamp_range(
+            start_sec, end_sec
+        )
+
+        epoch_events = [DepositEvent.from_dict(event) for event in epoch_events]
+        sorted_events = sorted(epoch_events, key=attrgetter("user", "timestamp"))
+
+        user_events = {
+            k: list(g) for k, g in groupby(sorted_events, key=attrgetter("user"))
+        }
+
+        for event in epoch_start_events:
+            if event.user in user_events:
+                user_events[event.user].insert(0, event)
+            else:
+                user_events[event.user] = [event]
+
+        epoch_start_users = list(map(attrgetter("user"), epoch_start_events))
+        for user_address in user_events:
+            if user_address not in epoch_start_users:
+                user_events[user_address].insert(
+                    0,
+                    DepositEvent(
+                        user_address,
+                        EventType.LOCK,
+                        timestamp=start_sec,
+                        amount=0,
+                        deposit_before=0,
+                    ),
+                )
+
+            user_events[user_address] = unify_deposit_balances(
+                user_events[user_address],
+                self.sablier_unlock_grace_period,
+            )
+
+        return user_events
diff --git a/backend/v2/epoch_snapshots/repositories.py b/backend/v2/epoch_snapshots/repositories.py
index b1e3117d90..c392a8cd9b 100644
--- a/backend/v2/epoch_snapshots/repositories.py
+++ b/backend/v2/epoch_snapshots/repositories.py
@@ -1,4 +1,7 @@
-from app.infrastructure.database.models import PendingEpochSnapshot
+from app.infrastructure.database.models import (
+    FinalizedEpochSnapshot,
+    PendingEpochSnapshot,
+)
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
@@ -10,3 +13,14 @@ async def get_pending_epoch_snapshot(
         select(PendingEpochSnapshot).filter(PendingEpochSnapshot.epoch == epoch_number)
     )
     return result.scalar_one_or_none()
+
+
+async def get_finalized_epoch_snapshot(
+    session: AsyncSession, epoch_number: int
+) -> FinalizedEpochSnapshot | None:
+    result = await session.execute(
+        select(FinalizedEpochSnapshot).filter(
+            FinalizedEpochSnapshot.epoch == epoch_number
+        )
+    )
+    return result.scalar_one_or_none()
diff --git a/backend/v2/epochs/contracts.py b/backend/v2/epochs/contracts.py
index 8ef263bc96..48c76c6fa7 100644
--- a/backend/v2/epochs/contracts.py
+++ b/backend/v2/epochs/contracts.py
@@ -1,5 +1,5 @@
 import logging
-from typing import Dict, Optional
+from typing import Optional
 
 from v2.core.contracts import SmartContract
 from web3 import exceptions
@@ -49,7 +49,7 @@ async def get_epoch_duration(self) -> int:
         logging.debug("[Epochs contract] Checking epoch duration")
         return await self.contract.functions.getEpochDuration().call()
 
-    async def get_future_epoch_props(self) -> Dict:
+    async def get_future_epoch_props(self) -> list[int]:
         logging.debug("[Epochs contract] Getting epoch props index")
         index = await self.contract.functions.epochPropsIndex().call()
         logging.debug("[Epochs contract] Getting next epoch props")
diff --git a/backend/v2/epochs/dependencies.py b/backend/v2/epochs/dependencies.py
index 67f391f599..f7ff2cc88e 100644
--- a/backend/v2/epochs/dependencies.py
+++ b/backend/v2/epochs/dependencies.py
@@ -8,7 +8,12 @@
 
 from app.exceptions import InvalidEpoch
 from app.modules.staking.proceeds.core import ESTIMATED_STAKING_REWARDS_RATE
-from v2.core.dependencies import OctantSettings, Web3
+from app.context.epoch_state import EpochState
+from v2.epoch_snapshots.repositories import (
+    get_finalized_epoch_snapshot,
+    get_pending_epoch_snapshot,
+)
+from v2.core.dependencies import GetSession, OctantSettings, Web3
 from v2.core.exceptions import AllocationWindowClosed
 from v2.core.transformers import transform_to_checksum_address
 from v2.core.types import Address
@@ -81,6 +86,34 @@ async def get_rewards_rate(epoch_number: int) -> float:
     return ESTIMATED_STAKING_REWARDS_RATE
 
 
+async def get_epoch_state(
+    session: GetSession, epochs_contracts: GetEpochsContracts, epoch_number: int
+) -> EpochState:
+    current_epoch_number = await epochs_contracts.get_current_epoch()
+    if epoch_number > current_epoch_number:
+        return EpochState.FUTURE
+
+    if epoch_number == current_epoch_number:
+        return EpochState.CURRENT
+
+    pending_epoch_number = await epochs_contracts.get_pending_epoch()
+    pending_snapshot = await get_pending_epoch_snapshot(session, epoch_number)
+    if epoch_number == pending_epoch_number:
+        if pending_snapshot is None:
+            return EpochState.PRE_PENDING
+        else:
+            return EpochState.PENDING
+
+    if pending_snapshot is None:
+        raise InvalidEpoch()
+
+    finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
+    if finalized_snapshot is None:
+        return EpochState.FINALIZING
+    else:
+        return EpochState.FINALIZED
+
+
 GetEpochsSubgraph = Annotated[
     EpochsSubgraph,
     Depends(get_epochs_subgraph),
diff --git a/backend/v2/epochs/subgraphs.py b/backend/v2/epochs/subgraphs.py
index 8ddfd8a91c..621389af55 100644
--- a/backend/v2/epochs/subgraphs.py
+++ b/backend/v2/epochs/subgraphs.py
@@ -2,8 +2,12 @@
 from dataclasses import dataclass
 from typing import Callable, Sequence, Type, Union
 
+from pydantic import TypeAdapter
 import backoff
 from app import exceptions
+from app.infrastructure.graphql.locks import LockEvent
+from app.infrastructure.graphql.unlocks import UnlockEvent
+from v2.core.types import OctantModel
 from v2.core.exceptions import EpochsNotFound
 from app.context.epoch.details import EpochDetails
 from gql import Client, gql
@@ -42,6 +46,14 @@ class BackoffParams:
     giveup: Callable[[Exception], bool] = lambda e: False
 
 
+class EpochSubgraphItem(OctantModel):
+    epoch: int
+    fromTs: int
+    toTs: int
+    duration: int
+    decisionWindow: int
+
+
 class EpochsSubgraph:
     def __init__(
         self,
@@ -66,14 +78,72 @@ def __init__(
                 self.gql_client.execute_async
             )
 
-    async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
-        """Get EpochDetails from the subgraph for a given epoch number."""
+    async def fetch_locks_by_timestamp_range(
+        self, from_ts: int, to_ts: int
+    ) -> list[LockEvent]:
+        """
+        Get locks by timestamp range.
+        """
+        query = gql(
+            """
+            query GetLocks($fromTimestamp: Int!, $toTimestamp: Int!) {
+              lockeds(
+                first: 1000,
+                skip: 0,
+                orderBy: timestamp
+                where: {timestamp_gte: $fromTimestamp, timestamp_lt: $toTimestamp}
+              ) {
+                __typename
+                depositBefore
+                amount
+                timestamp
+                user
+                transactionHash
+              }
+            }
+            """
+        )
+        variables = {
+            "fromTimestamp": from_ts,
+            "toTimestamp": to_ts,
+        }
+        response = await self.gql_client.execute_async(query, variable_values=variables)
+        return response["lockeds"]
 
-        logging.debug(
-            f"[Subgraph] Getting epoch properties for epoch number: {epoch_number}"
+    async def fetch_unlocks_by_timestamp_range(
+        self, from_ts: int, to_ts: int
+    ) -> list[UnlockEvent]:
+        """
+        Get unlocks by timestamp range.
+        """
+        query = gql(
+            """
+            query GetUnlocks($fromTimestamp: Int!, $toTimestamp: Int!) {
+              unlockeds(
+                first: 1000,
+                skip: 0,
+                orderBy: timestamp
+                where: {timestamp_gte: $fromTimestamp, timestamp_lt: $toTimestamp}
+              ) {
+                __typename
+                depositBefore
+                amount
+                timestamp
+                user
+                transactionHash
+              }
+            }
+            """
         )
+        variables = {
+            "fromTimestamp": from_ts,
+            "toTimestamp": to_ts,
+        }
+        response = await self.gql_client.execute_async(query, variable_values=variables)
+        return response["unlockeds"]
 
-        # Prepare query and variables
+    async def fetch_epoch_by_number(self, epoch_number: int) -> EpochSubgraphItem:
+        """Get EpochDetails from the subgraph for a given epoch number."""
         query = gql(
             """\
             query GetEpoch($epochNo: Int!) {
@@ -85,31 +155,28 @@ async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
                 decisionWindow
             }
             }
-        """
+            """
         )
         variables = {"epochNo": epoch_number}
 
-        # Execute query
         response = await self.gql_client.execute_async(query, variable_values=variables)
 
-        # Raise exception if no data received
         data = response["epoches"]
         if not data:
-            logging.warning(
-                f"[Subgraph] No epoch properties received for epoch number: {epoch_number}"
-            )
             raise exceptions.EpochNotIndexed(epoch_number)
 
-        # Parse response and return result
-        logging.debug(f"[Subgraph] Received epoch properties: {data[0]}")
+        return TypeAdapter(EpochSubgraphItem).validate_python(data[0])
 
-        epoch_details = data[0]
+    async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
+        """Get EpochDetails from the subgraph for a given epoch number."""
+
+        epoch_details = await self.fetch_epoch_by_number(epoch_number)
 
         return EpochDetails(
-            epoch_num=epoch_details["epoch"],
-            start=epoch_details["fromTs"],
-            duration=epoch_details["duration"],
-            decision_window=epoch_details["decisionWindow"],
+            epoch_num=epoch_details.epoch,
+            start=epoch_details.fromTs,
+            duration=epoch_details.duration,
+            decision_window=epoch_details.decisionWindow,
             remaining_sec=0,
         )
 
diff --git a/backend/v2/glms/contracts.py b/backend/v2/glms/contracts.py
index 4a167009be..8b5d45c8f8 100644
--- a/backend/v2/glms/contracts.py
+++ b/backend/v2/glms/contracts.py
@@ -29,7 +29,10 @@ async def approve(self, owner: AddressKey, benefactor_address, wad: int):
             benefactor_address, wad
         ).build_transaction({"from": owner.address, "nonce": nonce})
         signed_tx = self.w3.eth.account.sign_transaction(transaction, owner.key)
-        return self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
+        return await self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
+
+    async def balance_of(self, owner: str) -> int:
+        return await self.contract.functions.balanceOf(owner).call()
 
 
 ERC20_ABI = [
diff --git a/backend/v2/glms/dependencies.py b/backend/v2/glms/dependencies.py
index 44debdc9ce..084efb1be4 100644
--- a/backend/v2/glms/dependencies.py
+++ b/backend/v2/glms/dependencies.py
@@ -1,6 +1,8 @@
+from __future__ import annotations
 from typing import Annotated
 
 from fastapi import Depends
+from v2.deposits.dependencies import GetDepositsContracts
 from v2.core.dependencies import OctantSettings, Web3
 from v2.glms.contracts import ERC20_ABI, GLMContracts
 
@@ -17,3 +19,14 @@ def get_glm_contracts(
     w3: Web3, settings: Annotated[GLMSettings, Depends(get_glm_settings)]
 ) -> GLMContracts:
     return GLMContracts(w3, ERC20_ABI, settings.glm_contract_address)  # type: ignore[arg-type]
+
+
+async def get_glm_balance_of_deposits(
+    glm_contracts: GetGLMContracts,
+    deposits_contracts: GetDepositsContracts,
+) -> int:
+    return await glm_contracts.balance_of(deposits_contracts.address)
+
+
+GetGLMContracts = Annotated[GLMContracts, Depends(get_glm_contracts)]
+GetGLMBalanceOfDeposits = Annotated[int, Depends(get_glm_balance_of_deposits)]
diff --git a/backend/v2/matched_rewards/dependencies.py b/backend/v2/matched_rewards/dependencies.py
index cdafa0ed3f..3abf80a79b 100644
--- a/backend/v2/matched_rewards/dependencies.py
+++ b/backend/v2/matched_rewards/dependencies.py
@@ -28,7 +28,7 @@ def get_matched_rewards_estimator_settings() -> MatchedRewardsEstimatorSettings:
     return MatchedRewardsEstimatorSettings()
 
 
-async def get_matched_rewards_estimator(
+async def get_matched_rewards_estimator_only_in_aw(
     epoch_number: GetOpenAllocationWindowEpochNumber,
     session: GetSession,
     epochs_subgraph: Annotated[EpochsSubgraph, Depends(get_epochs_subgraph)],
@@ -47,6 +47,30 @@ async def get_matched_rewards_estimator(
     )
 
 
+async def get_matched_rewards_estimator(
+    epoch_number: int,
+    session: GetSession,
+    epochs_subgraph: Annotated[EpochsSubgraph, Depends(get_epochs_subgraph)],
+    settings: Annotated[
+        MatchedRewardsEstimatorSettings,
+        Depends(get_matched_rewards_estimator_settings),
+    ],
+) -> MatchedRewardsEstimator:
+    return MatchedRewardsEstimator(
+        session=session,
+        epochs_subgraph=epochs_subgraph,
+        tr_percent=settings.TR_PERCENT,
+        ire_percent=settings.IRE_PERCENT,
+        matched_rewards_percent=settings.MATCHED_REWARDS_PERCENT,
+        epoch_number=epoch_number,
+    )
+
+
+GetMatchedRewardsEstimatorInAW = Annotated[
+    MatchedRewardsEstimator,
+    Depends(get_matched_rewards_estimator_only_in_aw),
+]
+
 GetMatchedRewardsEstimator = Annotated[
     MatchedRewardsEstimator,
     Depends(get_matched_rewards_estimator),
diff --git a/backend/v2/project_rewards/capped_quadriatic.py b/backend/v2/project_rewards/capped_quadratic.py
similarity index 88%
rename from backend/v2/project_rewards/capped_quadriatic.py
rename to backend/v2/project_rewards/capped_quadratic.py
index 8fa3197c94..1c3b27e9e1 100644
--- a/backend/v2/project_rewards/capped_quadriatic.py
+++ b/backend/v2/project_rewards/capped_quadratic.py
@@ -5,24 +5,24 @@
 
 from v2.allocations.schemas import AllocationWithUserUQScore
 from v2.core.types import Address
-from v2.project_rewards.schemas import ProjectFundingSummary
+from v2.project_rewards.schemas import ProjectFundingSummaryV1
 
 
-class CappedQuadriaticFunding(NamedTuple):
-    project_fundings: dict[Address, ProjectFundingSummary]
-    amounts_total: Decimal  # Sum of all allocation amounts for all projects
-    matched_total: Decimal  # Sum of all matched rewards for all projects
+class CappedQuadraticFunding(NamedTuple):
+    project_fundings: dict[Address, ProjectFundingSummaryV1]
+    allocations_total_for_all_projects: Decimal
+    matched_total_for_all_projects: Decimal
 
 
 MR_FUNDING_CAP_PERCENT = Decimal("0.2")
 
 
-def capped_quadriatic_funding(
+def capped_quadratic_funding(
     allocations: list[AllocationWithUserUQScore],
     matched_rewards: int,
     project_addresses: list[str],
     MR_FUNDING_CAP_PERCENT: Decimal = MR_FUNDING_CAP_PERCENT,
-) -> CappedQuadriaticFunding:
+) -> CappedQuadraticFunding:
     """
     Calculate capped quadratic funding based on a list of allocations.
 
@@ -33,7 +33,7 @@ def capped_quadriatic_funding(
         MR_FUNDING_CAP_PERCENT (float, optional): The maximum percentage of matched rewards that any single project can receive. Defaults to MR_FUNDING_CAP_PERCENT.
 
     Returns:
-        CappedQuadriaticFunding: A named tuple containing the total and per-project amounts and matched rewards.
+        CappedQuadraticFunding: A named tuple containing the total and per-project amounts and matched rewards.
     """
 
     # Group allocations by project
@@ -96,7 +96,7 @@ def capped_quadriatic_funding(
         matched_total += matched_capped
 
     project_fundings = {
-        project_address: ProjectFundingSummary(
+        project_address: ProjectFundingSummaryV1(
             address=project_address,
             allocated=int(amount_by_project[project_address]),
             matched=int(matched_by_project[project_address]),
@@ -104,10 +104,10 @@ def capped_quadriatic_funding(
         for project_address in project_addresses
     }
 
-    return CappedQuadriaticFunding(
+    return CappedQuadraticFunding(
         project_fundings=project_fundings,
-        amounts_total=amounts_total,
-        matched_total=matched_total,
+        allocations_total_for_all_projects=amounts_total,
+        matched_total_for_all_projects=matched_total,
     )
 
 
@@ -121,8 +121,8 @@ def cqf_calculate_total_leverage(matched_rewards: int, total_allocated: int) ->
 def cqf_calculate_individual_leverage(
     new_allocations_amount: int,
     project_addresses: list[Address],
-    before_allocation: CappedQuadriaticFunding,
-    after_allocation: CappedQuadriaticFunding,
+    before_allocation: CappedQuadraticFunding,
+    after_allocation: CappedQuadraticFunding,
 ) -> float:
     """Calculate the leverage of a user's new allocations in capped quadratic funding.
 
@@ -174,13 +174,13 @@ def cqf_simulate_leverage(
     ]
 
     # Calculate capped quadratic funding before and after the user's allocation
-    before_allocation = capped_quadriatic_funding(
+    before_allocation = capped_quadratic_funding(
         allocations_without_user,
         matched_rewards,
         project_addresses,
         MR_FUNDING_CAP_PERCENT,
     )
-    after_allocation = capped_quadriatic_funding(
+    after_allocation = capped_quadratic_funding(
         allocations_without_user + new_allocations,
         matched_rewards,
         project_addresses,
diff --git a/backend/v2/project_rewards/dependencies.py b/backend/v2/project_rewards/dependencies.py
index fad325071e..f98bd6d933 100644
--- a/backend/v2/project_rewards/dependencies.py
+++ b/backend/v2/project_rewards/dependencies.py
@@ -3,7 +3,7 @@
 from fastapi import Depends
 from v2.core.dependencies import GetSession
 from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimatorInAW
 from v2.project_rewards.services import ProjectRewardsEstimator
 from v2.projects.dependencies import GetProjectsContracts
 
@@ -12,7 +12,7 @@ async def get_project_rewards_estimator(
     epoch_number: GetOpenAllocationWindowEpochNumber,
     session: GetSession,
     projects_contracts: GetProjectsContracts,
-    estimated_project_matched_rewards: GetMatchedRewardsEstimator,
+    estimated_project_matched_rewards: GetMatchedRewardsEstimatorInAW,
 ) -> ProjectRewardsEstimator:
     return ProjectRewardsEstimator(
         session=session,
diff --git a/backend/v2/project_rewards/repositories.py b/backend/v2/project_rewards/repositories.py
new file mode 100644
index 0000000000..9836d6fecc
--- /dev/null
+++ b/backend/v2/project_rewards/repositories.py
@@ -0,0 +1,37 @@
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+from app.infrastructure.database.models import Reward
+from v2.core.types import Address
+
+
+async def get_rewards_for_epoch(
+    session: AsyncSession, epoch_number: int
+) -> list[Reward]:
+    """
+    Get all rewards for a given epoch.
+    """
+
+    results = await session.scalars(
+        select(Reward)
+        .where(Reward.epoch == epoch_number)
+        .order_by(Reward.address.asc())
+    )
+
+    return list(results.all())
+
+
+async def get_rewards_for_projects_in_epoch(
+    session: AsyncSession, epoch_number: int, projects_addresses: list[Address]
+) -> list[Reward]:
+    """
+    Get all rewards for a given epoch for a list of projects.
+    """
+
+    results = await session.scalars(
+        select(Reward)
+        .where(Reward.epoch == epoch_number)
+        .where(Reward.address.in_(projects_addresses))
+        .order_by(Reward.address.asc())
+    )
+
+    return list(results.all())
diff --git a/backend/v2/project_rewards/router.py b/backend/v2/project_rewards/router.py
index 7a68f8e531..fd6838e835 100644
--- a/backend/v2/project_rewards/router.py
+++ b/backend/v2/project_rewards/router.py
@@ -1,22 +1,492 @@
-from fastapi import APIRouter
+from fastapi import APIRouter, Response, status
+from app.context.epoch_state import EpochState
+from app.exceptions import (
+    InvalidEpoch,
+    MissingSnapshot,
+    NotImplementedForGivenEpochState,
+)
+from app.engine.user.budget.with_ppf import UserBudgetWithPPF
+from app.modules.snapshots.pending.core import calculate_user_budgets
+from app.modules.staking.proceeds.core import estimate_staking_proceeds
+from v2.matched_rewards.dependencies import (
+    GetMatchedRewardsEstimator,
+)
+from v2.deposits.dependencies import GetDepositEventsRepository
+from v2.glms.dependencies import GetGLMBalanceOfDeposits
+from v2.allocations.repositories import (
+    get_donors_for_epoch,
+    sum_allocations_by_epoch,
+)
+from v2.core.types import Address
+from v2.epoch_snapshots.repositories import get_finalized_epoch_snapshot
+from v2.epochs.dependencies import (
+    GetEpochsContracts,
+    GetEpochsSubgraph,
+    get_epoch_state,
+)
+from v2.project_rewards.repositories import get_rewards_for_projects_in_epoch
+from v2.projects.dependencies import (
+    GetProjectsAllocationThresholdGetter,
+    GetProjectsContracts,
+)
+from v2.user_patron_mode.repositories import (
+    get_all_patrons_at_timestamp,
+    get_all_users_budgets_by_epoch,
+    get_budget_by_user_address_and_epoch,
+)
+from v2.core.dependencies import GetCurrentTimestamp, GetSession
+from v2.project_rewards.services import (
+    calculate_effective_deposits,
+    calculate_octant_rewards,
+    calculate_user_budget,
+    get_rewards_merkle_tree_for_epoch,
+    simulate_user_effective_deposits,
+)
 from v2.project_rewards.dependencies import GetProjectRewardsEstimator
-from v2.project_rewards.schemas import EstimatedProjectRewardsResponse
+from v2.project_rewards.schemas import (
+    EpochBudgetItemV1,
+    EpochBudgetsResponseV1,
+    EstimatedBudgetByDaysRequestV1,
+    EstimatedBudgetByEpochRequestV1,
+    EstimatedProjectRewardsResponseV1,
+    ProjectFundingSummaryV1,
+    RewardsLeverageResponseV1,
+    RewardsMerkleTreeResponseV1,
+    ThresholdResponseV1,
+    UnusedRewardsResponseV1,
+    UpcomingUserBudgetResponseV1,
+    UserBudgetResponseV1,
+    UserBudgetWithMatchedFundingResponseV1,
+)
 
-api = APIRouter(prefix="/rewards", tags=["Allocations"])
+api = APIRouter(prefix="/rewards", tags=["Rewards"])
+
+
+@api.get("/budget/{user_address}/epoch/{epoch_number}")
+async def get_user_budget_for_epoch_v1(
+    session: GetSession,
+    user_address: Address,
+    epoch_number: int,
+    response: Response,
+) -> UserBudgetResponseV1 | None:
+    """
+    Returns user's rewards budget available to allocate for given epoch.
+    Returns 204 No Content if user does not have budget for given epoch.
+    """
+    budget = await get_budget_by_user_address_and_epoch(
+        session,
+        user_address,
+        epoch_number,
+    )
+
+    if budget is None:
+        response.status_code = status.HTTP_204_NO_CONTENT
+        return None
+
+    return UserBudgetResponseV1(budget=budget)
+
+
+@api.get("/budget/{user_address}/upcoming")
+async def get_user_budget_for_upcoming_epoch_v1(
+    # Dependencies
+    epochs_contracts: GetEpochsContracts,
+    epochs_subgraph: GetEpochsSubgraph,
+    deposit_events_repository: GetDepositEventsRepository,
+    current_timestamp: GetCurrentTimestamp,
+    # Parameters
+    user_address: Address,
+) -> UpcomingUserBudgetResponseV1:
+    """
+    Returns the upcoming user budget based on as-if allocation happened now.
+
+    This is done by simulating the epoch end as if it ended now.
+    So we simulate the pending snapshot for this epoch end.
+     - Get current epoch details
+     - Simulate the epoch end as if it ended now (end_time = now)
+     - Simulate the pending snapshot for this epoch end
+        - Estimate staking proceeds
+        - Calculate total effective deposit based on deposit events
+        - Calculate octant rewards
+     - Calculate the user budget for this pending snapshot
+    """
+
+    # Get current epoch details
+    epoch_number = await epochs_contracts.get_current_epoch()
+    epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
+
+    # We SIMULATE the epoch end as if it ended now
+    epoch_end = current_timestamp
+    epoch_start = epoch_details.fromTs
+
+    # BEGIN: Calculate pending snapshot
+    eth_proceeds = estimate_staking_proceeds(epoch_end - epoch_start)
+    # Based on all deposit events, calculate effective deposits
+    events = await deposit_events_repository.get_all_users_events(
+        epoch_number, epoch_start, epoch_end
+    )
+    user_deposits, total_effective_deposit = calculate_effective_deposits(
+        epoch_start, epoch_end, events
+    )
+
+    # Calculate octant rewards
+    rewards = calculate_octant_rewards(eth_proceeds, total_effective_deposit)
+
+    # END: Calculate pending snapshot
+
+    # Calculate user budget
+    user_budget_calculator = UserBudgetWithPPF()
+    user_budgets = calculate_user_budgets(
+        user_budget_calculator, rewards, user_deposits
+    )
+    user_budget = next(
+        (
+            budget.budget
+            for budget in user_budgets
+            if budget.user_address == user_address
+        ),
+        0,  # Default value if user not found
+    )
+
+    return UpcomingUserBudgetResponseV1(upcoming_budget=user_budget)
+
+
+@api.get("/budgets/epoch/{epoch_number}")
+async def get_epoch_budgets_v1(
+    session: GetSession,
+    epoch_number: int,
+) -> EpochBudgetsResponseV1:
+    """
+    Returns all users rewards budgets for the epoch.
+    """
+    budgets = await get_all_users_budgets_by_epoch(session, epoch_number)
+
+    return EpochBudgetsResponseV1(
+        budgets=[
+            EpochBudgetItemV1(address=address, amount=amount)
+            for address, amount in budgets.items()
+        ]
+    )
+
+
+@api.post("/estimated_budget")
+async def get_estimated_budget_v1(
+    session: GetSession,
+    epoch_contracts: GetEpochsContracts,
+    # Parameters
+    glm_balance: GetGLMBalanceOfDeposits,
+    request: EstimatedBudgetByEpochRequestV1,
+) -> UserBudgetWithMatchedFundingResponseV1:
+    """
+    Returns the estimated budget if the user deposits GLMs for a given number of FULL epochs.
+    Each epoch is a 90 day period.
+
+    This is done based on the last finalized snapshot.
+    - Get the last finalized epoch
+    - Calculate the leverage based on the last finalized epoch
+    - Get the future epoch details (start_time, duration, end_time)
+    - Calculate rewards:
+        - Estimate staking proceeds
+        - Assume total effective deposit as total GLMs from contract
+        - Calculate octant rewards
+    - Simulate user events as if they deposited given amount of GLMs
+    - Calculate user budget for this pending snapshot
+    """
+
+    future_epoch = await epoch_contracts.get_future_epoch_props()
+    epoch_start = future_epoch[2]
+    epoch_duration = future_epoch[3]
+    epoch_end = epoch_start + epoch_duration
+    epoch_remaining = epoch_duration
+
+    # We interpolate future rewards based on the future epoch
+    #  duration and total effective deposit of the finalized epoch
+    eth_proceeds = estimate_staking_proceeds(epoch_duration)
+    total_effective_deposit = glm_balance
+    future_rewards = calculate_octant_rewards(eth_proceeds, total_effective_deposit)
+
+    # Simulate budget
+    user_effective_deposit = simulate_user_effective_deposits(
+        epoch_start,
+        epoch_end,
+        epoch_remaining,
+        epoch_duration,
+        request.glm_amount,
+    )
+    user_budget = calculate_user_budget(
+        user_effective_deposit,
+        future_rewards,
+    )
+
+    epochs_budget = request.number_of_epochs * user_budget
+
+    # Matching fund is calculated based on the last epoch's leverage
+    epoch_number = await epoch_contracts.get_finalized_epoch()
+
+    # Calculate the leverage based on the last finalized epoch
+    finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
+    if finalized_snapshot is None:
+        raise MissingSnapshot()
+    matched_rewards = int(finalized_snapshot.matched_rewards)
+
+    allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
+    leverage = matched_rewards / allocations_sum if allocations_sum else 0
+
+    return UserBudgetWithMatchedFundingResponseV1(
+        budget=epochs_budget,
+        matched_funding=int(epochs_budget * leverage),
+    )
+
+
+@api.post("/estimated_budget/by_days")
+async def get_estimated_budget_by_days_v1(
+    epoch_contracts: GetEpochsContracts,
+    epoch_subgraph: GetEpochsSubgraph,
+    deposit_events: GetDepositEventsRepository,
+    glm_balance: GetGLMBalanceOfDeposits,
+    current_timestamp: GetCurrentTimestamp,
+    request: EstimatedBudgetByDaysRequestV1,
+) -> UserBudgetResponseV1:
+    """
+    Returns the estimated budget if the user deposits GLMs for a given number of days.
+
+    This is done by first filling up the current epoch and then calculating the remaining budget for the future epochs.
+    """
+
+    remaining_lock_duration = request.lock_duration_sec
+
+    # Get the current epoch details
+    current_epoch_number = await epoch_contracts.get_current_epoch()
+    current_epoch_details = await epoch_subgraph.fetch_epoch_by_number(
+        current_epoch_number
+    )
+    current_epoch_start = current_epoch_details.fromTs
+    current_epoch_duration = current_epoch_details.duration
+    current_epoch_remaining = (
+        current_epoch_start + current_epoch_duration - current_timestamp
+    )
+
+    # CURRENT EPOCH REWARDS
+    current_eth_proceeds = estimate_staking_proceeds(current_epoch_duration)
+    current_events = await deposit_events.get_all_users_events(
+        current_epoch_number,
+        current_epoch_start,
+        current_epoch_start + current_epoch_duration,
+    )
+    _, total_effective_deposit = calculate_effective_deposits(
+        current_epoch_start,
+        current_epoch_start + current_epoch_duration,
+        current_events,
+    )
+
+    current_rewards = calculate_octant_rewards(
+        current_eth_proceeds, total_effective_deposit
+    )
+
+    # Simulate user budget till the end of the current epoch
+    user_effective_deposit = simulate_user_effective_deposits(
+        current_epoch_start,
+        current_epoch_start + current_epoch_duration,
+        current_epoch_remaining,
+        remaining_lock_duration,
+        request.glm_amount,
+    )
+    budget = calculate_user_budget(
+        user_effective_deposit,
+        current_rewards,
+    )
+
+    # Subtract the current epoch remaining from the remaining lock duration
+    # If there's no remaining lock duration, we're done
+    remaining_lock_duration -= current_epoch_remaining
+    if remaining_lock_duration <= 0:
+        return UserBudgetResponseV1(budget=budget)
+
+    # FUTURE EPOCH REWARDS
+    # Because we've already filled up the current epoch, we need to calculate based on the future epoch
+    future_epoch_number = await epoch_contracts.get_future_epoch_props()
+    future_epoch_start = future_epoch_number[2]
+    future_epoch_duration = future_epoch_number[3]
+    future_epoch_end = future_epoch_start + future_epoch_duration
+    future_eth_proceeds = estimate_staking_proceeds(future_epoch_duration)
+
+    future_total_effective_deposit = glm_balance
+    future_rewards = calculate_octant_rewards(
+        future_eth_proceeds, future_total_effective_deposit
+    )
+
+    # Calculate the number of full future epochs and the remaining lock duration
+    full_epochs_num, remaining_future_epoch_sec = divmod(
+        remaining_lock_duration, future_epoch_duration
+    )
+
+    # Simulate user budget for the full future epochs
+    user_effective_deposit = simulate_user_effective_deposits(
+        future_epoch_start,
+        future_epoch_end,
+        future_epoch_duration,
+        future_epoch_duration,
+        request.glm_amount,
+    )
+    budget += full_epochs_num * calculate_user_budget(
+        user_effective_deposit,
+        future_rewards,
+    )
+
+    if remaining_future_epoch_sec > 0:
+        user_effective_deposit = simulate_user_effective_deposits(
+            future_epoch_start,
+            future_epoch_end,
+            future_epoch_duration,
+            remaining_future_epoch_sec,
+            request.glm_amount,
+        )
+        budget += calculate_user_budget(
+            user_effective_deposit,
+            future_rewards,
+        )
+
+    return UserBudgetResponseV1(budget=budget)
+
+
+@api.get("/leverage/{epoch_number}")
+async def get_rewards_leverage_v1(
+    session: GetSession,
+    epochs_contracts: GetEpochsContracts,
+    matched_rewards_estimator: GetMatchedRewardsEstimator,
+    # Parameters
+    epoch_number: int,
+) -> RewardsLeverageResponseV1:
+    """
+    Returns leverage for a given epoch.
+
+    For finalized epochs it returns the leverage based on the data from the finalized snapshot.
+    For pending and finalizing epochs it returns the leverage based on the estimated matched rewards of pending epoch.
+    """
+
+    epoch_state = await get_epoch_state(session, epochs_contracts, epoch_number)
+
+    # This operations only makes sense for finalized, finalizing and pending epochs
+    if epoch_state > EpochState.PENDING:
+        raise NotImplementedForGivenEpochState()
+
+    # Figure out the matched rewards
+    if epoch_state == EpochState.FINALIZED:
+        # For finalized epoch matched_rewards is just taken from the finalized snapshot
+        finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
+        if finalized_snapshot is None:
+            raise MissingSnapshot()
+        matched_rewards = int(finalized_snapshot.matched_rewards)
+
+    else:
+        # For pending or finalizing epoch matched_rewards is estimated based on current state of AW
+        matched_rewards = await matched_rewards_estimator.get()
+
+    allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
+
+    return RewardsLeverageResponseV1(
+        leverage=matched_rewards / allocations_sum if allocations_sum else 0
+    )
+
+
+@api.get("/merkle_tree/{epoch_number}")
+async def get_rewards_merkle_tree_v1(
+    session: GetSession,
+    epoch_number: int,
+) -> RewardsMerkleTreeResponseV1:
+    """
+    Returns the rewards merkle tree for a given epoch.
+    """
+
+    tree = await get_rewards_merkle_tree_for_epoch(session, epoch_number)
+    if tree is None:
+        # TODO: this could be better :)
+        raise InvalidEpoch()
+
+    return tree
 
 
 @api.get("/projects/estimated")
-async def get_estimated_project_rewards(
+async def get_estimated_project_rewards_v1(
     project_rewards_estimator: GetProjectRewardsEstimator,
-) -> EstimatedProjectRewardsResponse:
+) -> EstimatedProjectRewardsResponseV1:
     """
     Returns foreach project current allocation sum and estimated matched rewards.
 
-    This endpoint is available only for the pending epoch state.
+    This endpoint is available only while allocation window is open.
     """
 
     estimated_funding = await project_rewards_estimator.get()
 
-    return EstimatedProjectRewardsResponse(
+    return EstimatedProjectRewardsResponseV1(
         rewards=[f for f in estimated_funding.project_fundings.values()]
     )
+
+
+@api.get("/projects/epoch/{epoch_number}")
+async def get_rewards_for_projects_in_epoch_v1(
+    session: GetSession,
+    projects_contracts: GetProjectsContracts,
+    epoch_number: int,
+) -> EstimatedProjectRewardsResponseV1:
+    """
+    Returns projects with matched rewards for a given finalized epoch.
+
+    """
+
+    # For epoch projects retrieve their rewards
+    projects = await projects_contracts.get_project_addresses(epoch_number)
+    rewards = await get_rewards_for_projects_in_epoch(session, epoch_number, projects)
+
+    return EstimatedProjectRewardsResponseV1(
+        rewards=[
+            ProjectFundingSummaryV1(
+                address=reward.address,
+                allocated=int(reward.amount) - int(reward.matched),
+                matched=int(reward.matched),
+            )
+            for reward in rewards
+        ]
+    )
+
+
+@api.get("/threshold/{epoch_number}")
+async def get_rewards_threshold_v1(
+    epoch_number: int,
+    projects_allocation_threshold_getter: GetProjectsAllocationThresholdGetter,
+) -> ThresholdResponseV1:
+    threshold = await projects_allocation_threshold_getter.get()
+
+    return ThresholdResponseV1(threshold=threshold)
+
+
+@api.get("/unused/{epoch_number}")
+async def get_unused_rewards_v1(
+    session: GetSession,
+    epoch_subgraph: GetEpochsSubgraph,
+    epoch_number: int,
+) -> UnusedRewardsResponseV1:
+    """
+    Returns list of users who didn't use their rewards in an epoch and the total sum of all unused rewards.
+
+    This is based on available budgets and excluding donors (users who have allocated funds) and patrons.
+    """
+
+    # All users budgets, donors and patrons for the epoch
+    budgets = await get_all_users_budgets_by_epoch(session, epoch_number)
+    donors = await get_donors_for_epoch(session, epoch_number)
+    epoch_details = await epoch_subgraph.get_epoch_by_number(epoch_number)
+    patrons = await get_all_patrons_at_timestamp(
+        session, epoch_details.finalized_timestamp.datetime()
+    )
+
+    # Exclude donors and patrons from the list of users who didn't use their rewards
+    unused_budgets = {
+        address: budget
+        for address, budget in budgets.items()
+        if address not in set(donors + patrons)
+    }
+
+    return UnusedRewardsResponseV1(
+        addresses=sorted(list(unused_budgets.keys())),
+        value=sum(unused_budgets.values()),
+    )
diff --git a/backend/v2/project_rewards/schemas.py b/backend/v2/project_rewards/schemas.py
index 5c53fce910..49e6cc5ffb 100644
--- a/backend/v2/project_rewards/schemas.py
+++ b/backend/v2/project_rewards/schemas.py
@@ -2,7 +2,7 @@
 from v2.core.types import Address, BigInteger, OctantModel
 
 
-class ProjectFundingSummary(OctantModel):
+class ProjectFundingSummaryV1(OctantModel):
     address: Address = Field(..., description="The address of the project")
     allocated: BigInteger = Field(
         ..., description="Sum of all allocation amounts for the project"
@@ -12,35 +12,93 @@ class ProjectFundingSummary(OctantModel):
     )
 
 
-class EstimatedProjectRewardsResponse(OctantModel):
-    rewards: list[ProjectFundingSummary] = Field(
+class EstimatedProjectRewardsResponseV1(OctantModel):
+    rewards: list[ProjectFundingSummaryV1] = Field(
         ..., description="List of project funding summaries"
     )
 
 
-# project_rewards = await project_rewards_estimator.get(pending_epoch_number)
-#     rewards = [
-#         {
-#             "address": project_address,
-#             "allocated": str(project_rewards.amounts_by_project[project_address]),
-#             "matched": str(project_rewards.matched_by_project[project_address]),
-#         }
-#         for project_address in project_rewards.amounts_by_project.keys()
-#     ]
-
-# @ns.doc(
-#     description="Returns project rewards with estimated matched rewards for the pending epoch"
-# )
-# @ns.response(
-#     200,
-#     "",
-# )
-# @ns.route("/projects/estimated")
-# class EstimatedProjectRewards(OctantResource):
-#     @ns.marshal_with(projects_rewards_model)
-#     def get(self):
-#         app.logger.debug("Getting project rewards for the pending epoch")
-#         project_rewards = get_estimated_project_rewards().rewards
-#         app.logger.debug(f"Project rewards in the pending epoch: {project_rewards}")
-
-#         return {"rewards": project_rewards}
+class EstimatedBudgetByEpochRequestV1(OctantModel):
+    number_of_epochs: int = Field(
+        ..., description="Number of epochs when GLM are locked", gt=0
+    )
+    glm_amount: BigInteger = Field(
+        ..., description="Amount of estimated GLM locked in wei", ge=0
+    )
+
+
+class UserBudgetWithMatchedFundingResponseV1(OctantModel):
+    budget: BigInteger = Field(..., description="User budget for given epoch in wei.")
+    matched_funding: BigInteger = Field(
+        ..., description="Matched funding for given epoch in wei."
+    )
+
+
+class EstimatedBudgetByDaysRequestV1(OctantModel):
+    days: int = Field(..., description="Number of days when GLM are locked")
+    glm_amount: BigInteger = Field(
+        ..., description="Amount of estimated GLM locked in wei"
+    )
+
+    @property
+    def lock_duration_sec(self) -> int:
+        return self.days * 24 * 60 * 60
+
+
+class RewardsMerkleTreeLeafV1(OctantModel):
+    address: Address = Field(..., description="User account or project address")
+    amount: BigInteger = Field(..., description="Assigned reward")
+
+
+class RewardsMerkleTreeResponseV1(OctantModel):
+    epoch: int = Field(..., description="Epoch number")
+    rewards_sum: BigInteger = Field(
+        ..., description="Sum of assigned rewards for epoch"
+    )
+    root: str = Field(..., description="Merkle Tree root for epoch")
+    leaves: list[RewardsMerkleTreeLeafV1] = Field(
+        ..., description="List of Merkle Tree leaves"
+    )
+    leaf_encoding: list[str] = Field(..., description="Merkle tree leaf encoding")
+
+
+class UserBudgetResponseV1(OctantModel):
+    budget: BigInteger = Field(..., description="User budget for given epoch in wei.")
+
+
+class EpochBudgetItemV1(OctantModel):
+    address: Address = Field(..., description="User address")
+    amount: BigInteger = Field(..., description="User budget for given epoch in wei.")
+
+
+class EpochBudgetsResponseV1(OctantModel):
+    budgets: list[EpochBudgetItemV1] = Field(
+        ..., description="List of user budgets for given epoch"
+    )
+
+
+class UpcomingUserBudgetResponseV1(OctantModel):
+    upcoming_budget: BigInteger = Field(
+        ..., description="Calculated upcoming user budget."
+    )
+
+
+class ThresholdResponseV1(OctantModel):
+    threshold: BigInteger = Field(
+        ...,
+        description="Threshold, that projects have to pass to be eligible for receiving rewards.",
+    )
+
+
+class RewardsLeverageResponseV1(OctantModel):
+    leverage: float = Field(..., description="Leverage of the allocated funds")
+
+
+class UnusedRewardsResponseV1(OctantModel):
+    addresses: list[Address] = Field(
+        ...,
+        description="List of addresses that neither allocated rewards nor toggled patron mode",
+    )
+    value: BigInteger = Field(
+        ..., description="Total unused rewards sum in an epoch (WEI)"
+    )
diff --git a/backend/v2/project_rewards/services.py b/backend/v2/project_rewards/services.py
index 3ef386d1fd..6ab93bdd6d 100644
--- a/backend/v2/project_rewards/services.py
+++ b/backend/v2/project_rewards/services.py
@@ -1,16 +1,81 @@
 from dataclasses import dataclass
+from multiproof import StandardMerkleTree
 
 from sqlalchemy.ext.asyncio import AsyncSession
-
+from app.constants import ZERO_ADDRESS
+from app.engine.octant_rewards import OctantRewardsSettings
+from app.engine.user.budget import UserBudgetPayload
+from app.engine.user.budget.with_ppf import UserBudgetWithPPF
+from app.modules.dto import OctantRewardsDTO
+from app.modules.octant_rewards.core import calculate_rewards
+from v2.project_rewards.repositories import get_rewards_for_epoch
+from v2.project_rewards.schemas import (
+    RewardsMerkleTreeLeafV1,
+    RewardsMerkleTreeResponseV1,
+)
 from v2.allocations.repositories import get_allocations_with_user_uqs
 from v2.matched_rewards.services import MatchedRewardsEstimator
-from v2.project_rewards.capped_quadriatic import (
-    CappedQuadriaticFunding,
-    capped_quadriatic_funding,
+from v2.project_rewards.capped_quadratic import (
+    CappedQuadraticFunding,
+    capped_quadratic_funding,
 )
 from v2.projects.contracts import ProjectsContracts
 
 
+from typing import Dict
+
+from app.engine.user.effective_deposit import (
+    DepositEvent,
+    EventType,
+    UserDeposit,
+    UserEffectiveDepositPayload,
+)
+from app.engine.user.effective_deposit.weighted_average.default import (
+    DefaultWeightedAverageEffectiveDeposit,
+)
+
+
+def calculate_effective_deposits(
+    start_sec: int,
+    end_sec: int,
+    events: Dict[str, list[DepositEvent]],
+) -> tuple[list[UserDeposit], int]:
+    # TODO: We can do this better and nicer
+    effective_deposit_calculator = DefaultWeightedAverageEffectiveDeposit()
+    payload = UserEffectiveDepositPayload(
+        epoch_start=start_sec,
+        epoch_end=end_sec,
+        lock_events_by_addr=events,
+    )
+
+    return effective_deposit_calculator.calculate_users_effective_deposits(payload)
+
+
+def simulate_user_events(
+    end_sec: int, lock_duration: int, remaining_sec: int, glm_amount: int
+) -> list[DepositEvent]:
+    user_events = [
+        DepositEvent(
+            user=ZERO_ADDRESS,
+            type=EventType.LOCK,
+            timestamp=end_sec - remaining_sec,
+            amount=glm_amount,
+            deposit_before=0,
+        )
+    ]
+    if lock_duration < remaining_sec:
+        user_events.append(
+            DepositEvent(
+                user=ZERO_ADDRESS,
+                type=EventType.UNLOCK,
+                timestamp=end_sec - remaining_sec + lock_duration,
+                amount=glm_amount,
+                deposit_before=glm_amount,
+            )
+        )
+    return user_events
+
+
 @dataclass
 class ProjectRewardsEstimator:
     # Dependencies
@@ -21,7 +86,7 @@ class ProjectRewardsEstimator:
     # Parameters
     epoch_number: int
 
-    async def get(self) -> CappedQuadriaticFunding:
+    async def get(self) -> CappedQuadraticFunding:
         # Gather all the necessary data for the calculation
         all_projects = await self.projects_contracts.get_project_addresses(
             self.epoch_number
@@ -34,8 +99,118 @@ async def get(self) -> CappedQuadriaticFunding:
         )
 
         # Calculate using the Capped Quadratic Funding formula
-        return capped_quadriatic_funding(
+        return capped_quadratic_funding(
             project_addresses=all_projects,
             allocations=allocations,
             matched_rewards=matched_rewards,
         )
+
+
+async def get_rewards_merkle_tree_for_epoch(
+    session: AsyncSession,
+    epoch_number: int,
+) -> RewardsMerkleTreeResponseV1 | None:
+    """
+    Get the rewards merkle tree for a given epoch.
+    """
+
+    # Merkle tree is based on rewards, so we need to get them first
+    rewards = await get_rewards_for_epoch(session, epoch_number)
+    if not rewards:
+        return None
+
+    # Build the merkle tree (using the leaf encoding)
+    LEAF_ENCODING: list[str] = ["address", "uint256"]
+    mt = StandardMerkleTree.of(
+        [[leaf.address, int(leaf.amount)] for leaf in rewards],
+        LEAF_ENCODING,
+    )
+
+    # Build response model
+    leaves = [
+        RewardsMerkleTreeLeafV1(address=leaf.value[0], amount=leaf.value[1])
+        for leaf in mt.values
+    ]
+
+    rewards_sum = sum(leaf.amount for leaf in leaves)
+
+    return RewardsMerkleTreeResponseV1(
+        epoch=epoch_number,
+        rewards_sum=rewards_sum,
+        root=mt.root,
+        leaves=leaves,
+        leaf_encoding=LEAF_ENCODING,
+    )
+
+
+def calculate_octant_rewards(
+    eth_proceeds: int,
+    total_effective_deposit: int,
+) -> OctantRewardsDTO:
+    """
+    Calculate the octant rewards based on ETH proceeds and total effective deposit.
+    Proceeds are the what the octant gets from the staking.
+    Effective deposit is the amount of GLMs that are eligible for rewards.
+
+    Parameters:
+        eth_proceeds: int - The amount of ETH proceeds.
+        total_effective_deposit: int - The total amount of GLMs deposited.
+
+    Returns:
+        OctantRewardsDTO - The octant rewards distribution.
+    """
+
+    settings = OctantRewardsSettings()
+    rewards = calculate_rewards(settings, eth_proceeds, total_effective_deposit)
+
+    return OctantRewardsDTO(
+        staking_proceeds=eth_proceeds,
+        locked_ratio=rewards.locked_ratio,
+        total_effective_deposit=total_effective_deposit,
+        total_rewards=rewards.total_rewards,
+        vanilla_individual_rewards=rewards.vanilla_individual_rewards,
+        operational_cost=rewards.operational_cost,
+        ppf=rewards.ppf_value,
+        community_fund=rewards.community_fund,
+    )
+
+
+def simulate_user_effective_deposits(
+    epoch_start: int,
+    epoch_end: int,
+    epoch_remaining: int,
+    lock_duration: int,
+    glm_amount: int,
+) -> int:
+    """
+    Simulate the user effective deposits for a given epoch.
+    """
+
+    events = {
+        ZERO_ADDRESS: simulate_user_events(
+            epoch_end, lock_duration, epoch_remaining, glm_amount
+        )
+    }
+    _, total_effective_deposit = calculate_effective_deposits(
+        epoch_start, epoch_end, events
+    )
+    return total_effective_deposit
+
+
+def calculate_user_budget(
+    user_effective_deposit: int,
+    rewards: OctantRewardsDTO,
+) -> int:
+    """
+    Calculate the user budget based on the user effective deposit and the octant rewards.
+    """
+
+    budget_calculator = UserBudgetWithPPF()
+    return budget_calculator.calculate_budget(
+        UserBudgetPayload(
+            user_effective_deposit=user_effective_deposit,
+            total_effective_deposit=rewards.total_effective_deposit,
+            vanilla_individual_rewards=rewards.vanilla_individual_rewards,
+            ppf=rewards.ppf,
+        )
+    )
diff --git a/backend/v2/projects/contracts.py b/backend/v2/projects/contracts.py
index 8656b1a7b1..4f5b733332 100644
--- a/backend/v2/projects/contracts.py
+++ b/backend/v2/projects/contracts.py
@@ -1,14 +1,19 @@
 import logging
 
+from v2.core.types import Address
+from v2.core.transformers import transform_to_checksum_address
 from v2.core.contracts import SmartContract
 
 
 class ProjectsContracts(SmartContract):
-    async def get_project_addresses(self, epoch_number: int) -> list[str]:
+    async def get_project_addresses(self, epoch_number: int) -> list[Address]:
         logging.debug(
             f"[Projects contract] Getting project addresses for epoch: {epoch_number}"
         )
-        return await self.contract.functions.getProposalAddresses(epoch_number).call()
+        addresses = await self.contract.functions.getProposalAddresses(
+            epoch_number
+        ).call()
+        return [transform_to_checksum_address(address) for address in addresses]
 
     async def get_project_cid(self) -> str:
         logging.debug("[Projects contract] Getting projects CID")
diff --git a/backend/v2/projects/dependencies.py b/backend/v2/projects/dependencies.py
index 116c919007..7dbe08868c 100644
--- a/backend/v2/projects/dependencies.py
+++ b/backend/v2/projects/dependencies.py
@@ -97,14 +97,21 @@ async def get_projects_metadata_getter(
 
 
 async def get_projects_details_getter(
-    session: GetSession, epochs: EpochsParameter, search_phrases: SearchPhrasesParameter
+    session: GetSession,
+    epochs: EpochsParameter,
+    search_phrases_param: SearchPhrasesParameter,
 ) -> ProjectsDetailsGetter:
-    epoch_numbers, search_phrases = process_search_params(epochs, search_phrases)
+    epoch_numbers, search_phrases = process_search_params(epochs, search_phrases_param)
     return ProjectsDetailsGetter(
         session=session, epoch_numbers=epoch_numbers, search_phrases=search_phrases
     )
 
 
+GetProjectsAllocationThresholdGetter = Annotated[
+    ProjectsAllocationThresholdGetter,
+    Depends(get_projects_allocation_threshold_getter),
+]
+
 GetProjectsContracts = Annotated[
     ProjectsContracts,
     Depends(get_projects_contracts),
diff --git a/backend/v2/sablier/__init__.py b/backend/v2/sablier/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/v2/sablier/dependencies.py b/backend/v2/sablier/dependencies.py
new file mode 100644
index 0000000000..2d59a3137d
--- /dev/null
+++ b/backend/v2/sablier/dependencies.py
@@ -0,0 +1,50 @@
+from functools import lru_cache
+from typing import Annotated
+
+from fastapi import Depends
+from pydantic import Field
+from app.shared.blockchain_types import ChainTypes
+from v2.sablier.subgraphs import SablierSubgraph
+from v2.core.dependencies import GetChainSettings, OctantSettings
+
+
+class SablierSubgraphSettings(OctantSettings):
+    # For mainnet
+    sablier_mainnet_subgraph_url: str
+    sablier_token_address: str = Field(
+        default="0x7DD9c5Cba05E151C895FDe1CF355C9A1D5DA6429",
+        alias="GLM_TOKEN_ADDRESS",
+    )
+    sablier_sender_address: str = ""
+    # For sepolia
+    sablier_sepolia_subgraph_url: str
+    sablier_token_address_sepolia: str = "0x71432dd1ae7db41706ee6a22148446087bdd0906"
+    sablier_sender_address_sepolia: str = "0xf86fD85672683c220709B9ED80bAD7a51800206a"
+
+
+@lru_cache(maxsize=1)
+def get_sablier_subgraph(chain_settings: GetChainSettings) -> SablierSubgraph:
+    """
+    Based on the chain type (mainnet or sepolia), return the appropriate SablierSubgraph.
+    """
+
+    sablier_settings = SablierSubgraphSettings()  # type: ignore[call-arg]
+
+    if chain_settings.chain_id == ChainTypes.MAINNET:
+        return SablierSubgraph(
+            sablier_settings.sablier_mainnet_subgraph_url,
+            sablier_settings.sablier_sender_address,
+            sablier_settings.sablier_token_address,
+        )
+
+    return SablierSubgraph(
+        sablier_settings.sablier_sepolia_subgraph_url,
+        sablier_settings.sablier_sender_address_sepolia,
+        sablier_settings.sablier_token_address_sepolia,
+    )
+
+
+GetSablierSubgraph = Annotated[
+    SablierSubgraph,
+    Depends(get_sablier_subgraph),
+]
diff --git a/backend/v2/sablier/subgraphs.py b/backend/v2/sablier/subgraphs.py
new file mode 100644
index 0000000000..c6fce77843
--- /dev/null
+++ b/backend/v2/sablier/subgraphs.py
@@ -0,0 +1,112 @@
+import logging
+from gql import Client, gql
+from gql.transport.aiohttp import AIOHTTPTransport
+from gql.client import log as requests_logger
+
+
+import backoff
+from app.infrastructure.sablier.events import SablierStream
+from v2.epochs.subgraphs import BackoffParams
+
+requests_logger.setLevel(logging.WARNING)
+
+
+class SablierSubgraph:
+    def __init__(
+        self,
+        url: str,
+        sender: str,
+        token_address: str,
+        backoff_params: BackoffParams | None = None,
+    ):
+        requests_logger.setLevel(logging.WARNING)
+        self.url = url
+        self.sender = sender
+        self.token_address = token_address
+
+        self.gql_client = Client(
+            transport=AIOHTTPTransport(
+                url=self.url,
+                timeout=2,
+            ),
+            fetch_schema_from_transport=False,
+        )
+
+        if backoff_params is not None:
+            backoff_decorator = backoff.on_exception(
+                backoff.expo,
+                backoff_params.exception,
+                max_time=backoff_params.max_time,
+                giveup=backoff_params.giveup,
+            )
+
+            self.gql_client.execute_async = backoff_decorator(
+                self.gql_client.execute_async
+            )
+
+    async def _fetch_streams(self, query: str, variables: dict) -> list[SablierStream]:
+        all_streams = []
+        has_more = True
+        limit = 1000
+        skip = 0
+
+        while has_more:
+            variables.update({"limit": limit, "skip": skip})
+
+            result = await self.gql_client.execute_async(
+                gql(query), variable_values=variables
+            )
+
+            streams = result.get("streams", [])
+
+            for stream in streams:
+                actions = stream.get("actions", [])
+                final_intact_amount = stream.get("intactAmount", 0)
+                all_streams.append(
+                    SablierStream(actions=actions, intactAmount=final_intact_amount)
+                )
+
+            if len(streams) < limit:
+                has_more = False
+            else:
+                skip += limit
+
+        return all_streams
+
+    async def get_all_streams_history(self) -> list[SablierStream]:
+        """
+        Get all the locks and unlocks in history.
+        """
+
+        query = """
+            query GetAllEvents($sender: String!, $tokenAddress: String!, $limit: Int!, $skip: Int!) {
+            streams(
+                where: {
+                sender: $sender
+                asset_: {address: $tokenAddress}
+                transferable: false
+                }
+                first: $limit
+                skip: $skip
+                orderBy: timestamp
+            ) {
+                id
+                intactAmount
+                actions(where: {category_in: [Cancel, Withdraw, Create]}, orderBy: timestamp) {
+                category
+                addressA
+                addressB
+                amountA
+                amountB
+                timestamp
+                hash
+                }
+            }
+            }
+        """
+        variables = {
+            "sender": self.sender,
+            "tokenAddress": self.token_address,
+        }
+
+        return await self._fetch_streams(query, variables)
diff --git a/backend/v2/user_patron_mode/repositories.py b/backend/v2/user_patron_mode/repositories.py
index 903660a5ef..422cc06832 100644
--- a/backend/v2/user_patron_mode/repositories.py
+++ b/backend/v2/user_patron_mode/repositories.py
@@ -4,13 +4,14 @@
 from sqlalchemy import Numeric, cast, func
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.future import select
-from v2.core.types import Address
+from v2.core.types import Address, BigInteger
 from v2.users.repositories import get_user_by_address
+from v2.core.transformers import transform_to_checksum_address
 
 
 async def get_all_patrons_at_timestamp(
     session: AsyncSession, dt: datetime
-) -> list[str]:
+) -> list[Address]:
     """
     Get all the user addresses that at given timestamp have patron_mode_enabled=True.
     """
@@ -27,7 +28,7 @@ async def get_all_patrons_at_timestamp(
         )
     )
 
-    return [row[0] for row in results.all()]
+    return [transform_to_checksum_address(row[0]) for row in results.all()]
 
 
 async def get_budget_sum_by_users_addresses_and_epoch(
@@ -87,6 +88,24 @@ async def get_budget_by_user_address_and_epoch(
     return int(budget)
 
 
+async def get_all_users_budgets_by_epoch(
+    session: AsyncSession, epoch_number: int
+) -> dict[Address, BigInteger]:
+    """
+    Get all budgets for a given epoch.
+    """
+    results = await session.execute(
+        select(Budget.budget, User.address)
+        .join(Budget.user)
+        .where(Budget.epoch == epoch_number)
+        .order_by(User.address)
+    )
+    return {
+        transform_to_checksum_address(row.address): int(row.budget)
+        for row in results.all()
+    }
+
+
 async def user_is_patron_with_budget(
     session: AsyncSession,
     user_address: Address,