diff --git a/arkindex/budget/__init__.py b/arkindex/budget/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/arkindex/budget/apps.py b/arkindex/budget/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..d24215cc34d6bd983d8525543a655aecbb90edff
--- /dev/null
+++ b/arkindex/budget/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BudgetConfig(AppConfig):
+    name = "arkindex.budget"
diff --git a/arkindex/budget/migrations/0001_initial.py b/arkindex/budget/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..67ebfb8b710210ec03d3f261aa2e0957c11d3459
--- /dev/null
+++ b/arkindex/budget/migrations/0001_initial.py
@@ -0,0 +1,44 @@
+# Generated by Django 5.0.8 on 2025-02-10 09:37
+
+import uuid
+
+import django.core.validators
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ("process", "0048_worker_cost_fields"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Budget",
+            fields=[
+                ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ("name", models.CharField(max_length=100, validators=[django.core.validators.MinLengthValidator(1)])),
+                ("total", models.DecimalField(decimal_places=3, default=0, max_digits=12)),
+            ],
+        ),
+        migrations.CreateModel(
+            name="BudgetEntry",
+            fields=[
+                ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ("created", models.DateTimeField(auto_now_add=True)),
+                ("description", models.CharField(max_length=255, validators=[django.core.validators.MinLengthValidator(1)])),
+                ("value", models.DecimalField(decimal_places=3, max_digits=12)),
+                ("budget", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="entries", to="budget.budget")),
+                ("process", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="budget_entries", to="process.process")),
+                ("user", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="budget_entries", to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                "verbose_name_plural": "budget entries",
+            },
+        ),
+    ]
diff --git a/arkindex/budget/migrations/__init__.py b/arkindex/budget/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/arkindex/budget/models.py b/arkindex/budget/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9567cb6ada63fe2881287abb46576215abc2d49
--- /dev/null
+++ b/arkindex/budget/models.py
@@ -0,0 +1,44 @@
+import uuid
+
+from django.conf import settings
+from django.core.validators import MinLengthValidator
+from django.db import models
+
+
+class Budget(models.Model):
+    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+    name = models.CharField(max_length=100, validators=[MinLengthValidator(1)])
+    total = models.DecimalField(max_digits=12, decimal_places=3, default=0)
+
+    def __str__(self) -> str:
+        return self.name
+
+
+class BudgetEntry(models.Model):
+    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+    created = models.DateTimeField(auto_now_add=True)
+
+    budget = models.ForeignKey(Budget, on_delete=models.CASCADE, related_name="entries")
+    description = models.CharField(max_length=255, validators=[MinLengthValidator(1)])
+    value = models.DecimalField(max_digits=12, decimal_places=3)
+
+    user = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        blank=True,
+        null=True,
+        on_delete=models.SET_NULL,
+        related_name="budget_entries",
+    )
+    process = models.ForeignKey(
+        "process.Process",
+        blank=True,
+        null=True,
+        on_delete=models.SET_NULL,
+        related_name="budget_entries",
+    )
+
+    class Meta:
+        verbose_name_plural = "budget entries"
+
+    def __str__(self) -> str:
+        return self.description
diff --git a/arkindex/documents/migrations/0014_corpus_budget.py b/arkindex/documents/migrations/0014_corpus_budget.py
new file mode 100644
index 0000000000000000000000000000000000000000..026c25cfb45183301b48072927efb9514456d19d
--- /dev/null
+++ b/arkindex/documents/migrations/0014_corpus_budget.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.0.8 on 2025-02-10 09:41
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("budget", "0001_initial"),
+        ("documents", "0013_corpus_maximum_task_ttl"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="corpus",
+            name="budget",
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="corpora", to="budget.budget"),
+        ),
+    ]
diff --git a/arkindex/documents/models.py b/arkindex/documents/models.py
index 6ea6ee7745c9cba8008808ddf6b669ae2ea007b9..e21ef63b10a23bd1f029ac5c12f1d17919a37567 100644
--- a/arkindex/documents/models.py
+++ b/arkindex/documents/models.py
@@ -59,6 +59,14 @@ class Corpus(IndexableModel):
                   "When not set, the instance-wide maximum time-to-live will be used instead.",
     )
 
+    budget = models.ForeignKey(
+        "budget.Budget",
+        on_delete=models.SET_NULL,
+        related_name="corpora",
+        blank=True,
+        null=True,
+    )
+
     # Specific manager for ACL
     objects = CorpusManager()
 
diff --git a/arkindex/documents/serializers/elements.py b/arkindex/documents/serializers/elements.py
index ee9ec29171e743ad7b17eaa618f0ea80d0c54ba1..528cd7b0d238b20c7a059be15a191073cf9001d9 100644
--- a/arkindex/documents/serializers/elements.py
+++ b/arkindex/documents/serializers/elements.py
@@ -285,6 +285,7 @@ class CorpusSerializer(serializers.ModelSerializer):
             "authorized_users",
             "indexable",
             "maximum_task_ttl",
+            "budget_id",
         )
         extra_kwargs = {
             "public": {
@@ -293,6 +294,9 @@ class CorpusSerializer(serializers.ModelSerializer):
             },
             "indexable": {
                 "help_text": "Whether this corpus should be indexed in Solr and searchable using the `SearchCorpus` endpoint."
+            },
+            "budget_id": {
+                "read_only": True,
             }
         }
 
diff --git a/arkindex/documents/tests/test_corpus.py b/arkindex/documents/tests/test_corpus.py
index 805447a33f06bf23f7ac7a47f022b69bac8db688..3055b5e8e67b7fb6787c53db95bb3568a9af3a51 100644
--- a/arkindex/documents/tests/test_corpus.py
+++ b/arkindex/documents/tests/test_corpus.py
@@ -104,6 +104,7 @@ class TestCorpus(FixtureAPITestCase):
                     "authorized_users": 1,
                     "top_level_type": None,
                     "maximum_task_ttl": 3600,
+                    "budget_id": None,
                 }
             ]
         )
@@ -138,6 +139,7 @@ class TestCorpus(FixtureAPITestCase):
                     "authorized_users": 2,
                     "top_level_type": None,
                     "maximum_task_ttl": 3600,
+                    "budget_id": None,
                 },
                 {
                     "id": str(self.corpus_public.id),
@@ -150,6 +152,7 @@ class TestCorpus(FixtureAPITestCase):
                     "authorized_users": 1,
                     "top_level_type": None,
                     "maximum_task_ttl": 3600,
+                    "budget_id": None,
                 }
             ]
         )
@@ -184,6 +187,7 @@ class TestCorpus(FixtureAPITestCase):
                     "authorized_users": 2,
                     "top_level_type": None,
                     "maximum_task_ttl": 3600,
+                    "budget_id": None,
                 },
                 {
                     "id": str(self.corpus_hidden.id),
@@ -196,6 +200,7 @@ class TestCorpus(FixtureAPITestCase):
                     "authorized_users": 0,
                     "top_level_type": None,
                     "maximum_task_ttl": 3600,
+                    "budget_id": None,
                 },
                 {
                     "id": str(self.corpus_public.id),
@@ -208,6 +213,7 @@ class TestCorpus(FixtureAPITestCase):
                     "authorized_users": 1,
                     "top_level_type": None,
                     "maximum_task_ttl": 3600,
+                    "budget_id": None,
                 }
             ]
         )
@@ -356,6 +362,7 @@ class TestCorpus(FixtureAPITestCase):
             "authorized_users": 1,
             "top_level_type": None,
             "maximum_task_ttl": 3600,
+            "budget_id": None,
         })
 
     def test_retrieve(self):
@@ -375,6 +382,7 @@ class TestCorpus(FixtureAPITestCase):
             "authorized_users": 2,
             "top_level_type": None,
             "maximum_task_ttl": 3600,
+            "budget_id": None,
         })
 
     def test_retrieve_not_found(self):
@@ -405,6 +413,7 @@ class TestCorpus(FixtureAPITestCase):
             "authorized_users": 1,
             "top_level_type": None,
             "maximum_task_ttl": 3600,
+            "budget_id": None,
         })
 
     @expectedFailure
@@ -434,6 +443,7 @@ class TestCorpus(FixtureAPITestCase):
             "authorized_users": 2,
             "top_level_type": None,
             "maximum_task_ttl": 3600,
+            "budget_id": None,
         })
 
     def test_retrieve_maximum_task_ttl(self):
diff --git a/arkindex/process/tests/process/test_destroy.py b/arkindex/process/tests/process/test_destroy.py
index b87856e50cc290f43f37fc638862e8f1f1c2c4d8..ffa37334ae10eafb3cc4e5c2229990564a78c87f 100644
--- a/arkindex/process/tests/process/test_destroy.py
+++ b/arkindex/process/tests/process/test_destroy.py
@@ -148,7 +148,7 @@ class TestProcessDestroy(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(19):
+        with self.assertNumQueries(20):
             response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.process.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -161,7 +161,7 @@ class TestProcessDestroy(FixtureAPITestCase):
         """
         self.client.force_login(self.superuser)
 
-        with self.assertNumQueries(19):
+        with self.assertNumQueries(20):
             response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.process.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -211,7 +211,7 @@ class TestProcessDestroy(FixtureAPITestCase):
             state=WorkerActivityState.Processed,
         )
 
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(21):
             response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.process.id}))
             self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
 
diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py
index b9fbcad8c7af6fb75c9cf77979ea5ca34f330e6c..fee1ef5ac2602f1ef00c67da2c85b36efe2bc350 100644
--- a/arkindex/project/settings.py
+++ b/arkindex/project/settings.py
@@ -127,6 +127,7 @@ INSTALLED_APPS = [
     "arkindex.users",
     "arkindex.process",
     "arkindex.training",
+    "arkindex.budget",
 ]
 
 MIDDLEWARE = [
diff --git a/arkindex/sql_validation/corpus_delete.sql b/arkindex/sql_validation/corpus_delete.sql
index bf5f22713085f427c02cea00062664ba4268beab..f789e6f8c7cd5ca1f78a0ef925f011695f106e65 100644
--- a/arkindex/sql_validation/corpus_delete.sql
+++ b/arkindex/sql_validation/corpus_delete.sql
@@ -6,7 +6,8 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."top_level_type_id",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
-       "documents_corpus"."maximum_task_ttl"
+       "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id"
 FROM "documents_corpus"
 WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
 LIMIT 21;
diff --git a/arkindex/sql_validation/corpus_delete_top_level_type.sql b/arkindex/sql_validation/corpus_delete_top_level_type.sql
index dbbe418e7389f4b1d9b79c37984632511f432571..d3ccdb0d2d4ceec1105a22f3a6e0dfbd97be161f 100644
--- a/arkindex/sql_validation/corpus_delete_top_level_type.sql
+++ b/arkindex/sql_validation/corpus_delete_top_level_type.sql
@@ -6,7 +6,8 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."top_level_type_id",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
-       "documents_corpus"."maximum_task_ttl"
+       "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id"
 FROM "documents_corpus"
 WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
 LIMIT 21;
diff --git a/arkindex/sql_validation/corpus_rights_filter.sql b/arkindex/sql_validation/corpus_rights_filter.sql
index 6456c7da350966b8be2f82ef0215810931153463..f914e4f909827dbddd3783b6a5e44ac19f9bb342 100644
--- a/arkindex/sql_validation/corpus_rights_filter.sql
+++ b/arkindex/sql_validation/corpus_rights_filter.sql
@@ -23,6 +23,7 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
        "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id",
        LEAST("users_right"."level", T5."level") AS "max_level"
 FROM "documents_corpus"
 INNER JOIN "users_right" ON ("documents_corpus"."id" = "users_right"."content_id"
diff --git a/arkindex/sql_validation/corpus_rights_filter_public.sql b/arkindex/sql_validation/corpus_rights_filter_public.sql
index 60427780297b6d7fb4ab6a5d61865cdbc63bc03e..d46840b4d4d4ed8862c234a1e7a708f7d139b113 100644
--- a/arkindex/sql_validation/corpus_rights_filter_public.sql
+++ b/arkindex/sql_validation/corpus_rights_filter_public.sql
@@ -24,6 +24,7 @@ LIMIT 21;
             "documents_corpus"."public",
             "documents_corpus"."indexable",
             "documents_corpus"."maximum_task_ttl",
+            "documents_corpus"."budget_id",
             LEAST("users_right"."level", T5."level") AS "max_level"
      FROM "documents_corpus"
      INNER JOIN "users_right" ON ("documents_corpus"."id" = "users_right"."content_id"
@@ -44,6 +45,7 @@ UNION
             "documents_corpus"."public",
             "documents_corpus"."indexable",
             "documents_corpus"."maximum_task_ttl",
+            "documents_corpus"."budget_id",
             10 AS "max_level"
      FROM "documents_corpus"
      WHERE "documents_corpus"."public")
diff --git a/arkindex/sql_validation/list_elements.sql b/arkindex/sql_validation/list_elements.sql
index 98e3284cfa465154415b194f505440a0ae88d3b5..2cca682e6af8bf240a703ea26bccaded0a327b61 100644
--- a/arkindex/sql_validation/list_elements.sql
+++ b/arkindex/sql_validation/list_elements.sql
@@ -6,7 +6,8 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."top_level_type_id",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
-       "documents_corpus"."maximum_task_ttl"
+       "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id"
 FROM "documents_corpus"
 WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
 LIMIT 21;
diff --git a/arkindex/sql_validation/process_elements_filter_ml_class.sql b/arkindex/sql_validation/process_elements_filter_ml_class.sql
index fb1e9d4389cea1361a85639375aad08f83c50b18..bc15a46955079de959819a7ad85860be0e2e951f 100644
--- a/arkindex/sql_validation/process_elements_filter_ml_class.sql
+++ b/arkindex/sql_validation/process_elements_filter_ml_class.sql
@@ -52,7 +52,8 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."top_level_type_id",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
-       "documents_corpus"."maximum_task_ttl"
+       "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id"
 FROM "documents_corpus"
 WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
 LIMIT 21;
diff --git a/arkindex/sql_validation/process_elements_filter_type.sql b/arkindex/sql_validation/process_elements_filter_type.sql
index a566bc4a782d58d2fe29839372458bf19dfb37d8..931808c1ed99f5d98d030add763a5103a9bbee20 100644
--- a/arkindex/sql_validation/process_elements_filter_type.sql
+++ b/arkindex/sql_validation/process_elements_filter_type.sql
@@ -52,7 +52,8 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."top_level_type_id",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
-       "documents_corpus"."maximum_task_ttl"
+       "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id"
 FROM "documents_corpus"
 WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
 LIMIT 21;
diff --git a/arkindex/sql_validation/process_elements_top_level.sql b/arkindex/sql_validation/process_elements_top_level.sql
index 77423e4582a0ca0620335a39ec096af91e4ee5b0..d10220c4fe27cfe33da7fe6b9eaf4aaf67543ff2 100644
--- a/arkindex/sql_validation/process_elements_top_level.sql
+++ b/arkindex/sql_validation/process_elements_top_level.sql
@@ -52,7 +52,8 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."top_level_type_id",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
-       "documents_corpus"."maximum_task_ttl"
+       "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id"
 FROM "documents_corpus"
 WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
 LIMIT 21;
diff --git a/arkindex/sql_validation/process_elements_with_image.sql b/arkindex/sql_validation/process_elements_with_image.sql
index 2b6d311781ba790d1e63af7d571895f2c1beba55..c2197f83183cabcbecd14237ad98baf87ed7fbfd 100644
--- a/arkindex/sql_validation/process_elements_with_image.sql
+++ b/arkindex/sql_validation/process_elements_with_image.sql
@@ -52,7 +52,8 @@ SELECT "documents_corpus"."created",
        "documents_corpus"."top_level_type_id",
        "documents_corpus"."public",
        "documents_corpus"."indexable",
-       "documents_corpus"."maximum_task_ttl"
+       "documents_corpus"."maximum_task_ttl",
+       "documents_corpus"."budget_id"
 FROM "documents_corpus"
 WHERE "documents_corpus"."id" = '{corpus_id}'::uuid
 LIMIT 21;