diff --git a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
index 61245654db6684cb8eaf6456a705f464cfe75d37..11454caf4ec4f1bf510f7f5bbd01081d21722863 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class BarcodeId(val value: Long)
+value class BarcodeId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..12058586ce2cf565037e214d3c0220ddb9ac2fe5
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt
@@ -0,0 +1,30 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+interface BarcodeModel {
+  val barcodeId: BarcodeId
+  val drinkId: DrinkId
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
index a7ce209dc7fb65fc629fa0ff8d3879134f861b90..4670a5f508c3481e33545108f39d1292bb12594a 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class DrinkId(val value: Long)
+value class DrinkId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt
similarity index 75%
rename from api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt
index c0d79e1746447b91a38fe269eafb78472f6990bc..5019a19205bd5416866be4e26a76769886d1167c 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt
@@ -22,19 +22,16 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete.v1
+package de.chaosdorf.mete.model
 
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
-@Serializable
-data class AuditResponseV1(
-  @SerialName("payments_sum")
-  val payments: Double,
-  @SerialName("deposits_sum")
-  val deposits: Double,
-  @SerialName("sum")
-  val total: Double,
-  @SerialName("audits")
-  val entries: List<AuditEntryModelV1>
-)
+interface DrinkModel {
+  val drinkId: DrinkId
+  val name: String
+  val active: Boolean
+  val volume: BigDecimal
+  val caffeine: Int?
+  val price: BigDecimal
+  val logoUrl: String
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8313d89c82dceb7a519b58deeb4bd193be14c109
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
@@ -0,0 +1,42 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+import java.math.BigDecimal
+
+interface MeteApi {
+  suspend fun getManifest(): PwaManifest?
+  suspend fun listTransactions(user: UserId? = null): TransactionSummaryModel
+  suspend fun listBarcodes(): List<BarcodeModel>
+  suspend fun getBarcode(id: BarcodeId): BarcodeModel?
+  suspend fun listDrinks(): List<DrinkModel>
+  suspend fun getDrink(id: DrinkId): DrinkModel?
+  suspend fun listUsers(): List<UserModel>
+  suspend fun getUser(id: UserId): UserModel?
+  suspend fun deposit(id: UserId, amount: BigDecimal)
+  suspend fun withdraw(id: UserId, amount: BigDecimal)
+  suspend fun purchase(id: UserId, drink: DrinkId)
+  suspend fun purchase(id: UserId, barcode: BarcodeId)
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt
index a120e4eeecd292d1a1e4ee8b843ca4c1b797049a..ee4da8b862b0aabf6838979c3d7939688d2127f6 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt
@@ -22,8 +22,8 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete.v1
+package de.chaosdorf.mete.model
 
-interface MeteApiFactory<T> {
-  fun newInstance(baseUrl: String): T
+interface MeteApiFactory {
+  fun newInstance(baseUrl: String): MeteApi
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be21ea4013e53123d21c83eae39e5fde34c0e01b
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt
@@ -0,0 +1,35 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+import java.math.BigDecimal
+
+interface TransactionSummaryModel {
+  val payments: BigDecimal
+  val deposits: BigDecimal
+  val total: BigDecimal
+  val entries: List<TransactionModel>
+}
+
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt
similarity index 97%
rename from api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt
index 7734a53237cc1ac1025219e912cfc7047ef4cc47..ce833d8b6237d77972b0f1aec96c5c30bf0e0dd3 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt
similarity index 97%
rename from api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt
index 2732335e5c80baef808284b78648b854f3f1a789..a3fad8799b82b824a885891c8464596cb0637a13 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
similarity index 90%
rename from api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
index d1725c4307a22b314cc66aeccdac97980edc0c69..8b3a85e9462b794b507b18efa18d6426fa8f79b9 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class AuditEntryId(val value: Long)
+value class TransactionId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt
similarity index 77%
rename from api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt
index b3cf90126b925d431e3a6d6b5c38090482e6c1c3..43bd5cb78589444f7e14b24403242acb202fa9c5 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt
@@ -22,19 +22,14 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete.v1
+package de.chaosdorf.mete.model
 
-import de.chaosdorf.mete.AuditEntryId
-import de.chaosdorf.mete.DrinkId
 import kotlinx.datetime.Instant
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
-@Serializable
-data class AuditEntryModelV1(
-  val id: AuditEntryId,
-  @SerialName("created_at")
-  val createdAt: Instant,
-  val difference: Double,
-  val drink: DrinkId?
-)
+interface TransactionModel {
+  val transactionId: TransactionId
+  val difference: BigDecimal
+  val drinkId: DrinkId?
+  val timestamp: Instant
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/UserId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
index 1f72b510a21243674af3e290c5aee0be9fef9541..0eaacf4cd8980a88c6345034e324d89468110f63 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class UserId(val value: Long)
+value class UserId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d46048b5dce582564c39cf671f0470adbf8de16
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt
@@ -0,0 +1,37 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+import java.math.BigDecimal
+
+interface UserModel {
+  val userId: UserId
+  val active: Boolean
+  val name: String
+  val email: String
+  val balance: BigDecimal
+  val audit: Boolean
+  val redirect: Boolean
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt b/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4e1a4489be4b9429a18eb06bfb869378d90c92ca
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt
@@ -0,0 +1,44 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.util
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import java.math.BigDecimal
+
+object BigDecimalSerializer : KSerializer<BigDecimal> {
+  override val descriptor: SerialDescriptor =
+    PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)
+
+  override fun serialize(encoder: Encoder, value: BigDecimal) =
+    encoder.encodeString(value.toPlainString())
+
+  override fun deserialize(decoder: Decoder): BigDecimal =
+    BigDecimal(decoder.decodeString())
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt b/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a64e3726507f064ea134a7be0d0cb971344770e1
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt
@@ -0,0 +1,41 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.util
+
+import okhttp3.Interceptor
+import okhttp3.Response
+
+object Code204Interceptor : Interceptor {
+  override fun intercept(chain: Interceptor.Chain): Response {
+    val request = chain.request()
+    val response = chain.proceed(request)
+
+    return if (response.code == 204) {
+      response.newBuilder().code(200).build()
+    } else {
+      response
+    }
+  }
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
index 93f8461efe0f3e6e8e93d758ea22ed98cda0e4b3..ea35acb4197211452e87a17a4660bb5edfce8463 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
@@ -24,12 +24,16 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.BarcodeId
-import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.model.BarcodeId
+import de.chaosdorf.mete.model.BarcodeModel
+import de.chaosdorf.mete.model.DrinkId
+import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 
 @Serializable
-data class BarcodeModelV1(
-  val id: BarcodeId,
-  val drink: DrinkId,
-)
+internal data class BarcodeModelV1(
+  @SerialName("id")
+  override val barcodeId: BarcodeId,
+  @SerialName("drink")
+  override val drinkId: DrinkId,
+) : BarcodeModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
index bba984b3d95d0622a87dd7b238c5ab85b028db72..30755a7165cd70c49500a8ba095f09acd4d9025f 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
@@ -24,19 +24,30 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.DrinkModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
 import kotlinx.datetime.Instant
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
 @Serializable
-data class DrinkModelV1(
-  val id: DrinkId,
-  val name: String,
+internal data class DrinkModelV1(
+  @SerialName("id")
+  override val drinkId: DrinkId,
+  @SerialName("name")
+  override val name: String,
+  @SerialName("active")
+  override val active: Boolean,
   @SerialName("bottle_size")
-  val bottleSize: Double,
-  val caffeine: Int?,
-  val price: Double,
+  @Serializable(with = BigDecimalSerializer::class)
+  override val volume: BigDecimal,
+  @SerialName("caffeine")
+  override val caffeine: Int?,
+  @SerialName("price")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val price: BigDecimal,
   @SerialName("logo_file_name")
   val logoFileName: String,
   @SerialName("created_at")
@@ -50,6 +61,5 @@ data class DrinkModelV1(
   @SerialName("logo_updated_at")
   val logoUpdatedAt: Instant,
   @SerialName("logo_url")
-  val logoUrl: String,
-  val active: Boolean
-)
+  override val logoUrl: String,
+) : DrinkModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
index d08da96ac03856aa36bc128445742e2f17e238cf..7c25ea6828c4a9ef0e2fd1a52192c315cca613ea 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
@@ -24,52 +24,71 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.PwaManifest
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.BarcodeId
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.MeteApi
+import de.chaosdorf.mete.model.PwaManifest
+import de.chaosdorf.mete.model.UserId
+import retrofit2.Response
 import retrofit2.http.GET
 import retrofit2.http.Path
 import retrofit2.http.Query
+import java.math.BigDecimal
 
-interface MeteApiV1 {
+internal interface MeteApiV1 : MeteApi {
   @GET("manifest.json")
-  suspend fun getManifest(): PwaManifest?
+  override suspend fun getManifest(): PwaManifest?
 
   @GET("api/v1/audits.json")
-  suspend fun getAudits(
-    @Query("user") user: Long? = null
-  ): AuditResponseV1
+  override suspend fun listTransactions(
+    @Query("user") user: UserId?
+  ): TransactionSummaryModelV1
 
   @GET("api/v1/barcodes.json")
-  suspend fun listBarcodes(): List<BarcodeModelV1>
+  override suspend fun listBarcodes(): List<BarcodeModelV1>
 
   @GET("api/v1/barcodes/{id}.json")
-  suspend fun getBarcode(): BarcodeModelV1?
+  override suspend fun getBarcode(
+    @Path("id") id: BarcodeId
+  ): BarcodeModelV1?
 
   @GET("api/v1/drinks.json")
-  suspend fun listDrinks(): List<DrinkModelV1>
+  override suspend fun listDrinks(): List<DrinkModelV1>
 
   @GET("api/v1/drinks/{id}.json")
-  suspend fun getDrink(@Path("id") id: DrinkId): DrinkModelV1?
+  override suspend fun getDrink(
+    @Path("id") id: DrinkId
+  ): DrinkModelV1?
 
   @GET("api/v1/users.json")
-  suspend fun listUsers(): List<UserModelV1>
+  override suspend fun listUsers(): List<UserModelV1>
 
   @GET("api/v1/users/{id}.json")
-  suspend fun getUser(@Path("id") id: UserId): UserModelV1?
+  override suspend fun getUser(
+    @Path("id") id: UserId
+  ): UserModelV1?
 
   @GET("api/v1/users/{id}/deposit.json")
-  suspend fun deposit(@Path("id") id: UserId)
+  override suspend fun deposit(
+    @Path("id") id: UserId,
+    @Query("amount") amount: BigDecimal
+  )
 
   @GET("api/v1/users/{id}/payment.json")
-  suspend fun payment(@Path("id") id: UserId)
+  override suspend fun withdraw(
+    @Path("id") id: UserId,
+    @Query("amount") amount: BigDecimal
+  )
 
   @GET("api/v1/users/{id}/buy.json")
-  suspend fun buy(@Path("id") id: UserId)
+  override suspend fun purchase(
+    @Path("id") id: UserId,
+    @Query("drink") drink: DrinkId
+  )
 
   @GET("api/v1/users/{id}/buy_barcode.json")
-  suspend fun buyWithBarcode(@Path("id") id: UserId)
-
-  @GET("api/v1/users/stats.json")
-  suspend fun getStats()
+  override suspend fun purchase(
+    @Path("id") id: UserId,
+    @Query("barcode") barcode: BarcodeId
+  )
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
index 2e51fec5877d10c5a8885ea4b1103471d25d1e55..3fdc434304fe940d8d8c6b1223df7c0e474e314a 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
@@ -25,19 +25,28 @@
 package de.chaosdorf.mete.v1
 
 import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import de.chaosdorf.mete.model.MeteApi
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.util.Code204Interceptor
 import kotlinx.serialization.json.Json
 import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
 import retrofit2.Retrofit
 import retrofit2.create
 
-object MeteApiV1Factory : MeteApiFactory<MeteApiV1> {
+object MeteApiV1Factory : MeteApiFactory {
   private val json = Json {
     ignoreUnknownKeys = true
   }
 
-  override fun newInstance(baseUrl: String): MeteApiV1 {
+  private val httpClient = OkHttpClient.Builder()
+    .addInterceptor(Code204Interceptor)
+    .build()
+
+  override fun newInstance(baseUrl: String): MeteApi {
     val contentType = "application/json".toMediaType()
     val retrofit = Retrofit.Builder()
+      .client(httpClient)
       .baseUrl(baseUrl)
       .addConverterFactory(json.asConverterFactory(contentType))
       .build()
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..02a1b7b565384889704252add85f7f16126447bf
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt
@@ -0,0 +1,47 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.TransactionId
+import de.chaosdorf.mete.model.TransactionModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
+import kotlinx.datetime.Instant
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.math.BigDecimal
+
+@Serializable
+internal data class TransactionModelV1(
+  @SerialName("id")
+  override val transactionId: TransactionId,
+  @SerialName("difference")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val difference: BigDecimal,
+  @SerialName("drink")
+  override val drinkId: DrinkId?,
+  @SerialName("created_at")
+  override val timestamp: Instant
+) : TransactionModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7edd2343b9c96fe7fc4585a2634d8448244289c5
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt
@@ -0,0 +1,46 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.model.TransactionSummaryModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.math.BigDecimal
+
+@Serializable
+internal data class TransactionSummaryModelV1(
+  @SerialName("payments_sum")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val payments: BigDecimal,
+  @SerialName("deposits_sum")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val deposits: BigDecimal,
+  @SerialName("sum")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val total: BigDecimal,
+  @SerialName("audits")
+  override val entries: List<TransactionModelV1>
+) : TransactionSummaryModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
index 33c76ca752c587270267ca5f6cca01f5e1e35ef1..75d6edccca00a13fd028679787d40cfcd4bdb27d 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
@@ -24,22 +24,33 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.mete.model.UserModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
 import kotlinx.datetime.Instant
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
 @Serializable
-data class UserModelV1(
-  val id: UserId,
-  val name: String,
-  val email: String,
+internal data class UserModelV1(
+  @SerialName("id")
+  override val userId: UserId,
+  @SerialName("name")
+  override val name: String,
+  @SerialName("email")
+  override val email: String,
+  @SerialName("balance")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val balance: BigDecimal,
+  @SerialName("active")
+  override val active: Boolean,
+  @SerialName("audit")
+  override val audit: Boolean,
+  @SerialName("redirect")
+  override val redirect: Boolean,
   @SerialName("created_at")
   val createdAt: Instant,
   @SerialName("updated_at")
   val updatedAt: Instant,
-  val balance: Double,
-  val active: Boolean,
-  val audit: Boolean,
-  val redirect: Boolean
-)
+) : UserModel
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e68570b35f3d3f16626d1d7b054277297ab2eba8..64e87db574fa38013b822565221a91c122f718f0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -38,6 +38,7 @@ android {
         getDefaultProguardFile("proguard-android.txt"),
         "proguard-rules.pro"
       )
+      signingConfig = signingConfigs.getByName("debug")
     }
 
     getByName("debug") {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
index 8d35e31128fafdc943f6f8be158020b3f0522f2c..c531e4c8ef1ef2fc291038aab8ba21e8c54ee72c 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
@@ -31,10 +31,12 @@ import dagger.Provides
 import dagger.hilt.InstallIn
 import dagger.hilt.android.qualifiers.ApplicationContext
 import dagger.hilt.components.SingletonComponent
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.v1.MeteApiV1Factory
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.PurchaseRepository
 import de.chaosdorf.meteroid.model.ServerRepository
+import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.model.UserRepository
 import javax.inject.Singleton
 
@@ -61,12 +63,15 @@ object DatabaseModule {
   ): UserRepository = database.users()
 
   @Provides
-  fun providePurchaseRepository(
+  fun provideTransactionRepository(
     database: MeteroidDatabase
-  ): PurchaseRepository = database.purchases()
+  ): TransactionRepository = database.transactions()
 
   @Provides
   fun provideServerRepository(
     database: MeteroidDatabase
   ): ServerRepository = database.server()
+
+  @Provides
+  fun providesMeteFactory(): MeteApiFactory = MeteApiV1Factory
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
index c29e736c162cbb4082ca818151c0415667082468..690b73c4ae6e870f337944de0061df471391c58c 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
@@ -25,44 +25,31 @@
 package de.chaosdorf.meteroid.sample
 
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.model.DrinkId
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.ServerId
-import kotlinx.datetime.Instant
 
 class SampleDrinkProvider : PreviewParameterProvider<Drink> {
   override val values = sequenceOf(
-      Drink(
-          serverId = ServerId(-1),
-          drinkId = DrinkId(27),
-          active = true,
-          name = "Club Mate",
-          volume = 0.5,
-          caffeine = null,
-          price = 1.5,
-          createdAt = Instant.fromEpochMilliseconds(1684598011800),
-          updatedAt = Instant.fromEpochMilliseconds(1684607122132),
-          logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png",
-          logoFileName = "logo.png",
-          logoContentType = "image/png",
-          logoFileSize = 183063,
-          logoUpdatedAt = Instant.fromEpochMilliseconds(1684607121995)
-      ),
-      Drink(
-          serverId = ServerId(-1),
-          drinkId = DrinkId(15),
-          active = false,
-          name = "Paulaner Spezi",
-          volume = 0.5,
-          caffeine = null,
-          price = 1.5,
-          createdAt = Instant.fromEpochMilliseconds(1684597806099),
-          updatedAt = Instant.fromEpochMilliseconds(1684607346944),
-          logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png",
-          logoFileName = "logo.png",
-          logoContentType = "image/png",
-          logoFileSize = 173265,
-          logoUpdatedAt = Instant.fromEpochMilliseconds(1684607346835)
-      )
+    Drink(
+      serverId = ServerId(-1),
+      drinkId = DrinkId(27),
+      active = true,
+      name = "Club Mate",
+      volume = 0.5.toBigDecimal(),
+      caffeine = null,
+      price = 1.5.toBigDecimal(),
+      logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png",
+    ),
+    Drink(
+      serverId = ServerId(-1),
+      drinkId = DrinkId(15),
+      active = false,
+      name = "Paulaner Spezi",
+      volume = 0.5.toBigDecimal(),
+      caffeine = null,
+      price = 1.5.toBigDecimal(),
+      logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png",
+    )
   )
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
index 75fdfb2c0b0a3711011d360533e1fdd3f41baf39..3d0751f4e636528100cd2f74526e2323f598df13 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
@@ -24,7 +24,7 @@
 
 package de.chaosdorf.meteroid.storage
 
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.ServerId
 import kotlinx.coroutines.flow.Flow
 
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
index 71a6be1d7ecb1b9215e474ab6bcbf1590a40da3e..62f4b809c7afbcad2bbd2a8ff02dd1875660a234 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
@@ -28,7 +28,7 @@ import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
 import androidx.datastore.preferences.core.longPreferencesKey
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.ServerId
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.mapLatest
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
index dc8e5ed99634ea494b8f9269b6255cb4bd049732..d9d2a162ff3c962aa0eac128b059985af0e1460d 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
@@ -25,8 +25,8 @@
 package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
@@ -35,6 +35,7 @@ import de.chaosdorf.meteroid.model.ServerId
 import javax.inject.Inject
 
 class DrinkSyncHandler @Inject constructor(
+  private val factory: MeteApiFactory,
   private val db: MeteroidDatabase,
   private val repository: DrinkRepository
 ) : SyncHandler<Server, Drink, DrinkSyncHandler.Key>() {
@@ -57,8 +58,8 @@ class DrinkSyncHandler @Inject constructor(
     repository.getAll(context.serverId)
 
   override suspend fun loadCurrent(context: Server): List<Drink> {
-    val api = MeteApiV1Factory.newInstance(context.url)
+    val api = factory.newInstance(context.url)
     val loadedEntries = api.listDrinks()
-    return loadedEntries.map { Drink.fromModelV1(context, it) }
+    return loadedEntries.map { Drink.fromModel(context, it) }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt
index 8ba6652d07b5d7958abab18f18b3813d4ce9fe2b..071c20c4881f368c6397e03c370e69cc1a6f6168 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt
@@ -25,42 +25,43 @@
 package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
-import de.chaosdorf.mete.AuditEntryId
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.TransactionId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.MeteroidDatabase
-import de.chaosdorf.meteroid.model.Purchase
-import de.chaosdorf.meteroid.model.PurchaseRepository
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
+import de.chaosdorf.meteroid.model.Transaction
+import de.chaosdorf.meteroid.model.TransactionRepository
 import javax.inject.Inject
 
-class PurchaseSyncHandler @Inject constructor(
+class TransactionSyncHandler @Inject constructor(
+  private val factory: MeteApiFactory,
   private val db: MeteroidDatabase,
-  private val repository: PurchaseRepository
-) : SyncHandler<Pair<Server, UserId>, Purchase, PurchaseSyncHandler.Key>() {
+  private val repository: TransactionRepository
+) : SyncHandler<Pair<Server, UserId>, Transaction, TransactionSyncHandler.Key>() {
   data class Key(
-    val server: ServerId, val purchase: AuditEntryId
+    val server: ServerId, val transaction: TransactionId
   )
 
   override suspend fun withTransaction(block: suspend () -> Unit) =
     db.withTransaction(block)
 
-  override suspend fun store(entry: Purchase) =
+  override suspend fun store(entry: Transaction) =
     repository.save(entry)
 
   override suspend fun delete(key: Key) =
-    repository.delete(key.server, key.purchase)
+    repository.delete(key.server, key.transaction)
 
-  override fun entryToKey(entry: Purchase) = Key(entry.serverId, entry.purchaseId)
+  override fun entryToKey(entry: Transaction) = Key(entry.serverId, entry.transactionId)
 
-  override suspend fun loadStored(context: Pair<Server, UserId>): List<Purchase> =
+  override suspend fun loadStored(context: Pair<Server, UserId>): List<Transaction> =
     repository.getAll(context.first.serverId, context.second)
 
-  override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Purchase> {
+  override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Transaction> {
     val (server, userId) = context
-    val api = MeteApiV1Factory.newInstance(server.url)
-    val loadedEntries = api.getAudits(user = userId.value).entries
-    return loadedEntries.map { Purchase.fromModelV1(server, userId, it) }
+    val api = factory.newInstance(server.url)
+    val loadedEntries = api.listTransactions(user = userId).entries
+    return loadedEntries.map { Transaction.fromModel(server, userId, it) }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
index a6f9291d3db387c6737d8f3f1ba64ca03d320124..179a5dbd20dd46c856f3ad6e99447bc1ceffad4b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
@@ -25,8 +25,8 @@
 package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
@@ -35,6 +35,7 @@ import de.chaosdorf.meteroid.model.UserRepository
 import javax.inject.Inject
 
 class UserSyncHandler @Inject constructor(
+  private val factory: MeteApiFactory,
   private val db: MeteroidDatabase,
   private val repository: UserRepository
 ) : SyncHandler<Server, User, UserSyncHandler.Key>() {
@@ -57,8 +58,8 @@ class UserSyncHandler @Inject constructor(
     repository.getAll(context.serverId)
 
   override suspend fun loadCurrent(context: Server): List<User> {
-    val api = MeteApiV1Factory.newInstance(context.url)
+    val api = factory.newInstance(context.url)
     val loadedEntries = api.listUsers()
-    return loadedEntries.map { User.fromModelV1(context, it) }
+    return loadedEntries.map { User.fromModel(context, it) }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
index 81b5e89523a17d84e13df25d394e7c4cc2650732..6b2e6540131ebd72501e8060defaa00c878ef57b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
@@ -26,6 +26,7 @@ package de.chaosdorf.meteroid.ui
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -34,21 +35,23 @@ import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavOptions
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.navigation
 import androidx.navigation.compose.rememberNavController
+import de.chaosdorf.meteroid.ui.Transactions.TransactionListScreen
 import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen
 import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel
 import de.chaosdorf.meteroid.ui.money.MoneyListScreen
 import de.chaosdorf.meteroid.ui.money.MoneyListViewModel
 import de.chaosdorf.meteroid.ui.navigation.Routes
-import de.chaosdorf.meteroid.ui.purchases.PurchaseListScreen
-import de.chaosdorf.meteroid.ui.purchases.PurchaseViewModel
 import de.chaosdorf.meteroid.ui.servers.AddServerScreen
 import de.chaosdorf.meteroid.ui.servers.ServerListScreen
+import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel
 import de.chaosdorf.meteroid.ui.users.UserListScreen
 import kotlinx.coroutines.launch
 
@@ -60,18 +63,37 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
 
   LaunchedEffect(initState) {
     when (initState) {
-      AppViewModel.InitState.LOADING -> navController.navigate(Routes.Init)
-      AppViewModel.InitState.CREATE_SERVER -> navController.navigate(Routes.Servers.Add)
-      AppViewModel.InitState.SELECT_SERVER -> navController.navigate(Routes.Servers.List)
-      AppViewModel.InitState.SELECT_USER -> navController.navigate(Routes.Users.List)
-      AppViewModel.InitState.HOME -> navController.navigate(Routes.Home.Root)
+      AppViewModel.InitState.LOADING -> navController.navigate(
+        Routes.Init,
+        NavOptions.Builder().setPopUpTo(Routes.Init, true).build()
+      )
+
+      AppViewModel.InitState.CREATE_SERVER -> navController.navigate(
+        Routes.Servers.Add,
+        NavOptions.Builder().setPopUpTo(Routes.Servers.Add, true).build()
+      )
+
+      AppViewModel.InitState.SELECT_SERVER -> navController.navigate(
+        Routes.Servers.List,
+        NavOptions.Builder().setPopUpTo(Routes.Servers.List, true).build()
+      )
+
+      AppViewModel.InitState.SELECT_USER -> navController.navigate(
+        Routes.Users.List,
+        NavOptions.Builder().setPopUpTo(Routes.Users.List, true).build()
+      )
+
+      AppViewModel.InitState.HOME -> navController.navigate(
+        Routes.Home.Root,
+        NavOptions.Builder().setPopUpTo(Routes.Home.Root, true).build()
+      )
     }
   }
 
   NavHost(navController, startDestination = Routes.Init) {
     composable(route = Routes.Init) { _ ->
-      Box(contentAlignment = Alignment.Center) {
-        Column {
+      Box(Modifier.fillMaxSize()) {
+        Column(Modifier.align(Alignment.Center)) {
           CircularProgressIndicator()
           Text("Loading")
         }
@@ -131,8 +153,8 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
         MoneyListScreen(moneyListViewModel, navController::navigate)
       }
       composable(Routes.Home.History) { _ ->
-        val purchaseViewModel = hiltViewModel<PurchaseViewModel>()
-        PurchaseListScreen(purchaseViewModel, navController::navigate)
+        val transactionViewModel = hiltViewModel<TransactionViewModel>()
+        TransactionListScreen(transactionViewModel, navController::navigate)
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
index bd066bb443087ce79a3443a72e69524010a02fc5..751902f50c52a6588f40ba9236367a68a61bebd5 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
@@ -27,14 +27,14 @@ package de.chaosdorf.meteroid.ui
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
 import de.chaosdorf.meteroid.storage.AccountPreferences
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.DrinkSyncHandler
-import de.chaosdorf.meteroid.sync.PurchaseSyncHandler
+import de.chaosdorf.meteroid.sync.TransactionSyncHandler
 import de.chaosdorf.meteroid.sync.UserSyncHandler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -51,9 +51,7 @@ class AppViewModel @Inject constructor(
   accountProvider: AccountProvider,
   private val accountPreferences: AccountPreferences,
   private val serverRepository: ServerRepository,
-  private val userSyncHandler: UserSyncHandler,
-  private val drinkSyncHandler: DrinkSyncHandler,
-  private val purchaseSyncHandler: PurchaseSyncHandler
+  private val syncManager: SyncManager
 ) : ViewModel() {
   val initState: StateFlow<InitState> = accountPreferences.state
     .flatMapLatest { preferences ->
@@ -69,12 +67,8 @@ class AppViewModel @Inject constructor(
 
   init {
     accountProvider.account.distinctUntilChanged().onEach { account ->
-      account?.let { (server, user) ->
-        userSyncHandler.sync(server)
-        drinkSyncHandler.sync(server)
-        user?.let { user ->
-          purchaseSyncHandler.sync(Pair(server, user.userId))
-        }
+      account?.let { (server, maybeUser) ->
+        syncManager.sync(server, maybeUser)
       }
     }.launchIn(viewModelScope)
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ded488064f050a2dae6b47c24ceb08ba5ee660d
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt
@@ -0,0 +1,62 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Badge
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import java.math.BigDecimal
+
+@Composable
+fun PriceBadge(
+  price: BigDecimal,
+  modifier: Modifier = Modifier,
+  containerColor: Color =
+    if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.primary
+    else MaterialTheme.colorScheme.error,
+  textColor: Color =
+    if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary
+    else MaterialTheme.colorScheme.onError,
+  textStyle: TextStyle = MaterialTheme.typography.labelLarge
+) {
+  Badge(
+    containerColor = containerColor,
+    modifier = modifier
+  ) {
+    Text(
+      "%.02f €".format(price),
+      style = textStyle,
+      color = textColor,
+      modifier = Modifier.padding(8.dp, 4.dp)
+    )
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..48276de223b25a2930a5d95c7a7185162f1e249d
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt
@@ -0,0 +1,75 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.meteroid.model.AccountInfo
+import de.chaosdorf.meteroid.model.Drink
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.sync.DrinkSyncHandler
+import de.chaosdorf.meteroid.sync.TransactionSyncHandler
+import de.chaosdorf.meteroid.sync.UserSyncHandler
+import java.math.BigDecimal
+import javax.inject.Inject
+
+class SyncManager @Inject constructor(
+  private val factory: MeteApiFactory,
+  private val userSyncHandler: UserSyncHandler,
+  private val drinkSyncHandler: DrinkSyncHandler,
+  private val transactionSyncHandler: TransactionSyncHandler
+) {
+  suspend fun sync(server: Server, user: User?) {
+    userSyncHandler.sync(server)
+    drinkSyncHandler.sync(server)
+    if (user != null) {
+      transactionSyncHandler.sync(Pair(server, user.userId))
+    }
+  }
+
+  suspend fun purchase(account: AccountInfo, drink: Drink) {
+    account.user?.let { user ->
+      val api = factory.newInstance(account.server.url)
+      api.purchase(user.userId, drink.drinkId)
+      sync(account.server, user)
+    }
+  }
+
+  suspend fun deposit(account: AccountInfo, amount: BigDecimal) {
+    account.user?.let { user ->
+      val api = factory.newInstance(account.server.url)
+      api.deposit(user.userId, amount)
+      sync(account.server, user)
+    }
+  }
+
+  suspend fun withdraw(account: AccountInfo, amount: BigDecimal) {
+    account.user?.let { user ->
+      val api = factory.newInstance(account.server.url)
+      api.withdraw(user.userId, amount)
+      sync(account.server, user)
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cdd66c369f44bffec4c57c3310ee314a4dc90064
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
@@ -0,0 +1,60 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.drinks
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun DrinkListFilterChip(
+  label: String,
+  selected: Boolean,
+  onClick: () -> Unit,
+) {
+  FilterChip(
+    label = {
+      Text(label, style = MaterialTheme.typography.labelLarge)
+    },
+    selected = selected,
+    leadingIcon = {
+      if (selected) {
+        Icon(
+          Icons.Default.Check,
+          contentDescription = null,
+          modifier = Modifier.size(18.dp)
+        )
+      }
+    },
+    onClick = onClick
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
index e0708c350d6b1158fa5404b9eea92a9aa111d82a..b872aef201dbb3264ce068cb1b82c92d86c360ac 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
@@ -26,12 +26,13 @@ package de.chaosdorf.meteroid.ui.drinks
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.Scaffold
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
@@ -39,11 +40,11 @@ import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavOptions
-import de.chaosdorf.meteroid.sync.SyncHandler
 import de.chaosdorf.meteroid.ui.navigation.HomeSections
 import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
 import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
 
+@OptIn(ExperimentalLayoutApi::class)
 @Composable
 fun DrinkListScreen(
   viewModel: DrinkListViewModel,
@@ -51,7 +52,8 @@ fun DrinkListScreen(
 ) {
   val account by viewModel.account.collectAsState()
   val drinks by viewModel.drinks.collectAsState()
-  val syncState by viewModel.syncState.collectAsState()
+  val filters by viewModel.filters.collectAsState()
+
   Scaffold(
     topBar = { MeteroidTopBar(account, onNavigate) },
     bottomBar = {
@@ -62,18 +64,28 @@ fun DrinkListScreen(
       )
     }
   ) { paddingValues: PaddingValues ->
-    Column {
-      if (syncState == SyncHandler.State.Loading) {
-        LinearProgressIndicator()
+    Column(Modifier.padding(paddingValues)) {
+      FlowRow(
+        modifier = Modifier.padding(horizontal = 16.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp)
+      ) {
+        DrinkListFilterChip(
+          label = "Active",
+          selected = filters.contains(DrinkListViewModel.Filter.Active),
+          onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.Active) }
+        )
+        DrinkListFilterChip(
+          label = "Coffeine Free",
+          selected = filters.contains(DrinkListViewModel.Filter.CaffeineFree),
+          onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.CaffeineFree) }
+        )
       }
       LazyVerticalGrid(
-        GridCells.Adaptive(120.dp),
-        modifier = Modifier.padding(paddingValues),
-        contentPadding = PaddingValues(12.dp),
-        horizontalArrangement = Arrangement.SpaceBetween,
+        GridCells.Adaptive(104.dp),
+        modifier = Modifier.padding(horizontal = 8.dp)
       ) {
         items(drinks) { drink ->
-          DrinkTile(drink)
+          DrinkTile(drink, viewModel::purchase)
         }
       }
     }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
index bca3d100d6dd6521c646a588775cc897b6a134d0..045d3757633f910dcec4f91aa6ae76b29371e698 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
@@ -24,39 +24,73 @@
 
 package de.chaosdorf.meteroid.ui.drinks
 
+import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.sync.AccountProvider
-import de.chaosdorf.meteroid.sync.DrinkSyncHandler
-import de.chaosdorf.meteroid.sync.SyncHandler
+import de.chaosdorf.meteroid.ui.SyncManager
+import de.chaosdorf.meteroid.util.update
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 @HiltViewModel
 class DrinkListViewModel @Inject constructor(
   accountProvider: AccountProvider,
   repository: DrinkRepository,
-  syncHandler: DrinkSyncHandler
+  private val syncManager: SyncManager,
+  private val savedStateHandle: SavedStateHandle
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
-  val drinks: StateFlow<List<Drink>> = accountProvider.account
-    .flatMapLatest { account ->
+  val filters: StateFlow<Set<Filter>> =
+    savedStateHandle.getStateFlow("filters", setOf(Filter.Active))
+
+  val drinks: StateFlow<List<Drink>> = combine(
+    accountProvider.account.flatMapLatest { account ->
       account?.let { (server, _) ->
         repository.getAllFlow(server.serverId)
       } ?: flowOf(emptyList())
-    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+    },
+    filters
+  ) { drinks, filters ->
+    drinks.filter { item ->
+      filters.all { filter -> filter.matches(item) }
+    }
+  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+
+  fun toggleFilter(filter: Filter) {
+    savedStateHandle.update<Set<Filter>>("filters", emptySet()) { filters ->
+      if (filters.contains(filter)) filters - filter
+      else filters + filter
+    }
+  }
+
+  fun purchase(item: Drink) {
+    account.value?.let { account ->
+      viewModelScope.launch {
+        syncManager.purchase(account, item)
+      }
+    }
+  }
+
+  enum class Filter {
+    CaffeineFree,
+    Active;
 
-  val syncState: StateFlow<SyncHandler.State> = syncHandler.state
+    fun matches(item: Drink) = when (this) {
+      CaffeineFree -> item.caffeine == 0
+      Active -> item.active
+    }
+  }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
index 2099f684419e787c81d533635ac1f1d1f5c170af..7c141b9503ddef01638d7deeedb333a2707852e5 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
@@ -26,13 +26,18 @@ package de.chaosdorf.meteroid.ui.drinks
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
@@ -40,21 +45,24 @@ import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
 import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.sample.SampleDrinkProvider
+import de.chaosdorf.meteroid.ui.PriceBadge
 
 @Preview(widthDp = 120, showBackground = true)
 @Composable
 fun DrinkTile(
-  @PreviewParameter(SampleDrinkProvider::class) item: Drink
+  @PreviewParameter(SampleDrinkProvider::class) item: Drink,
+  onPurchase: (Drink) -> Unit = {}
 ) {
   val thumbPainter = rememberAsyncImagePainter(
     item.logoUrl
@@ -65,7 +73,12 @@ fun DrinkTile(
   )
 
   Column(
-    modifier = Modifier.padding(4.dp)
+    modifier = Modifier
+      .height(IntrinsicSize.Max)
+      .alpha(if (item.active) 1.0f else 0.67f)
+      .clip(RoundedCornerShape(8.dp))
+      .clickable { onPurchase(item) }
+      .padding(8.dp)
   ) {
     Box {
       Image(
@@ -73,39 +86,38 @@ fun DrinkTile(
         contentDescription = null,
         contentScale = ContentScale.Fit,
         modifier = Modifier
+          .aspectRatio(1.0f)
           .clip(CircleShape)
           .background(MaterialTheme.colorScheme.primaryContainer)
-          .aspectRatio(1.0f)
-          .padding(8.dp)
       )
-      Text(
-        String.format("%.02f €", item.price),
-        color = MaterialTheme.colorScheme.onPrimary,
-        fontWeight = FontWeight.SemiBold,
+      PriceBadge(
+        item.price,
         modifier = Modifier
-          .padding(vertical = 12.dp)
-          .clip(RoundedCornerShape(16.dp))
-          .background(MaterialTheme.colorScheme.primary)
           .align(Alignment.BottomEnd)
-          .padding(horizontal = 8.dp)
+          .paddingFromBaseline(bottom = 12.dp)
       )
     }
+    Spacer(Modifier.height(4.dp))
     Text(
       item.name,
-      fontWeight = FontWeight.SemiBold,
-      maxLines = 1,
-      overflow = TextOverflow.Ellipsis,
-      modifier = Modifier.padding(horizontal = 4.dp)
-    )
-    Row(
-      horizontalArrangement = Arrangement.SpaceBetween,
       modifier = Modifier
-        .padding(horizontal = 4.dp)
         .fillMaxWidth()
-    ) {
+        .padding(horizontal = 8.dp),
+      textAlign = TextAlign.Center,
+      fontWeight = FontWeight.SemiBold,
+      style = MaterialTheme.typography.labelLarge,
+    )
+    Spacer(Modifier.height(4.dp))
+    Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
       Text(
-        String.format("%.02f l", item.volume),
-        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
+        String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume),
+        modifier = Modifier
+          .fillMaxWidth()
+          .padding(horizontal = 8.dp),
+        textAlign = TextAlign.Center,
+        fontWeight = FontWeight.SemiBold,
+        style = MaterialTheme.typography.labelMedium,
+        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
       )
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
index 14386cef5430a9d6a8393a024a497042d40828a0..78b8d667b6ae4abe6dc5577ea427d696e9eec2fb 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
@@ -67,7 +67,7 @@ fun MoneyListScreen(
         horizontalArrangement = Arrangement.SpaceBetween,
       ) {
         items(viewModel.money) { monetaryAmount ->
-          MoneyTile(monetaryAmount)
+          MoneyTile(monetaryAmount, viewModel::deposit)
         }
       }
     }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
index 508ccec5a62e2b86e3f6e0b5799a5a690e755ce8..7ba2e82985c2c5dd2dbf348d2263fcca492ca4c5 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
@@ -28,30 +28,43 @@ import androidx.annotation.DrawableRes
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
+import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.R
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.sync.AccountProvider
+import de.chaosdorf.meteroid.ui.SyncManager
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import java.math.BigDecimal
 import javax.inject.Inject
 
-enum class MonetaryAmount(val amount: Double, @DrawableRes val image: Int) {
-  MONEY_50(0.50, R.drawable.euro_50),
-  MONEY_100(1.00, R.drawable.euro_100),
-  MONEY_200(2.00, R.drawable.euro_200),
-  MONEY_500(5.00, R.drawable.euro_500),
-  MONEY_1000(10.00, R.drawable.euro_1000),
-  MONEY_2000(20.00, R.drawable.euro_2000),
-  MONEY_5000(50.00, R.drawable.euro_5000),
+enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) {
+  MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50),
+  MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100),
+  MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200),
+  MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500),
+  MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000),
+  MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000),
+  MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000),
 }
 
 @HiltViewModel
 class MoneyListViewModel @Inject constructor(
-  accountProvider: AccountProvider
+  accountProvider: AccountProvider,
+  private val syncManager: SyncManager
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
   val money: List<MonetaryAmount> = MonetaryAmount.entries
+
+  fun deposit(item: MonetaryAmount) {
+    account.value?.let { account ->
+      viewModelScope.launch {
+        syncManager.deposit(account, item.amount)
+      }
+    }
+  }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
index 5ec0760bd633730e1cd07cba42ab38933b5e4d49..5254c6f925c62aa400fce52cba059e773f5f927b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
@@ -26,50 +26,47 @@ package de.chaosdorf.meteroid.ui.money
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.paddingFromBaseline
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.ui.PriceBadge
 
 @Composable
 fun MoneyTile(
-  item: MonetaryAmount
+  item: MonetaryAmount,
+  onDeposit: (MonetaryAmount) -> Unit = {}
 ) {
-  Column(
-    modifier = Modifier.padding(4.dp)
+  Box(
+    modifier = Modifier
+      .height(IntrinsicSize.Max)
+      .clip(RoundedCornerShape(8.dp))
+      .clickable { onDeposit(item) }
   ) {
-    Box {
-      Image(
-        painterResource(item.image),
-        contentDescription = null,
-        contentScale = ContentScale.Fit,
-        modifier = Modifier
-          .aspectRatio(1.0f)
-          .padding(8.dp)
-      )
-      Text(
-        String.format("%.02f €", item.amount),
-        color = MaterialTheme.colorScheme.onPrimary,
-        fontWeight = FontWeight.SemiBold,
-        modifier = Modifier
-          .padding(vertical = 12.dp)
-          .clip(RoundedCornerShape(16.dp))
-          .background(MaterialTheme.colorScheme.primary)
-          .align(Alignment.BottomEnd)
-          .padding(horizontal = 8.dp)
-      )
-    }
+    Image(
+      painterResource(item.image),
+      contentDescription = null,
+      contentScale = ContentScale.Fit,
+      modifier = Modifier
+        .aspectRatio(1.0f)
+    )
+    PriceBadge(
+      item.amount,
+      modifier = Modifier
+        .align(Alignment.BottomEnd)
+        .paddingFromBaseline(bottom = 24.dp)
+    )
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
index e796d4f902d46000b4572c7cded48531ec5452f3..3a5d95e85a3da122ca99cf6b4287be9c20cd99ba 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
@@ -25,93 +25,94 @@
 package de.chaosdorf.meteroid.ui.navigation
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarColors
-import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import androidx.navigation.NavOptions
 import coil.compose.AsyncImage
 import de.chaosdorf.meteroid.model.AccountInfo
-import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.ui.PriceBadge
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.internal.toCanonicalHost
 
 @Composable
 fun MeteroidTopBar(
   account: AccountInfo?,
   onNavigate: (String, NavOptions) -> Unit
 ) {
-  TopAppBar(
-    title = {
-      Text(
-        account?.user?.name
-          ?: account?.server?.name
-          ?: "Meteroid"
+  Surface(
+    modifier = Modifier.padding(8.dp),
+    color = MaterialTheme.colorScheme.surface,
+    shadowElevation = 6.dp,
+    tonalElevation = 6.dp,
+    shape = RoundedCornerShape(32.dp),
+    onClick = {
+      onNavigate(Routes.Users.List, NavOptions.Builder().build())
+    }
+  ) {
+    Row(modifier = Modifier.padding(8.dp)) {
+      AsyncImage(
+        account?.user?.gravatarUrl(),
+        contentDescription = "User List",
+        contentScale = ContentScale.Crop,
+        modifier = Modifier
+          .size(48.dp)
+          .clip(CircleShape)
+          .background(MaterialTheme.colorScheme.tertiary)
       )
-    },
-    navigationIcon = {
-        AsyncImage(
-          account?.user?.gravatarUrl(),
-          contentDescription = "User List",
-          contentScale = ContentScale.Crop,
-          modifier = Modifier
-            .padding(start = 6.dp, end = 24.dp)
-            .size(40.dp)
-            .clip(CircleShape)
-            .border(1.dp, Color.White, CircleShape)
-            .background(MaterialTheme.colorScheme.primaryContainer)
-        )
-    },
-    actions = {
+      Spacer(Modifier.width(16.dp))
+      Column(modifier = Modifier.align(Alignment.CenterVertically)) {
+        if (account != null) {
+          if (account.user != null) {
+            Text(
+              account.user!!.name,
+              fontWeight = FontWeight.SemiBold
+            )
+            Text(
+              account.server.url.toHttpUrl().host,
+              color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
+              fontWeight = FontWeight.Medium
+            )
+          } else {
+            Text(
+              account.server.url.toHttpUrl().host,
+              fontWeight = FontWeight.SemiBold
+            )
+          }
+        } else {
+          Text(
+            "Meteroid",
+            fontWeight = FontWeight.SemiBold
+          )
+        }
+      }
+      Spacer(
+        Modifier
+          .weight(1.0f)
+          .width(16.dp))
       account?.user?.let { user ->
-        val (foreground, background) =
-          if (user.balance < 0)
-            Pair(MaterialTheme.colorScheme.onError, MaterialTheme.colorScheme.error)
-          else
-            Pair(MaterialTheme.colorScheme.onPrimary, MaterialTheme.colorScheme.primary)
-
-        Text(
-          String.format("%.02f €", user.balance),
-          color = foreground,
-          fontSize = 14.sp,
-          lineHeight = 20.sp,
-          fontWeight = FontWeight.SemiBold,
+        PriceBadge(
+          user.balance,
           modifier = Modifier
-            .padding(end = 20.dp)
-            .clip(RoundedCornerShape(16.dp))
-            .background(background)
-            .padding(horizontal = 8.dp)
+            .align(Alignment.CenterVertically)
+            .padding(end = 12.dp)
         )
       }
-    },
-    modifier = Modifier
-      .clickable {
-        onNavigate(
-          Routes.Users.Root,
-          NavOptions
-            .Builder()
-            .build()
-        )
-      },
-    colors = TopAppBarDefaults.topAppBarColors(
-      containerColor = MaterialTheme.colorScheme.primaryContainer,
-      titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
-    )
-  )
+    }
+  }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
index 3689ee0d1a02a3b9c7cf08dabb87f01c9e308822..7950b83ed59f9bed43d443a29b2801bc66ba6da2 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
@@ -27,7 +27,7 @@ package de.chaosdorf.meteroid.ui.servers
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
@@ -44,6 +44,7 @@ import kotlin.time.Duration.Companion.milliseconds
 
 @HiltViewModel
 class AddServerViewModel @Inject constructor(
+  private val factory: MeteApiFactory,
   private val repository: ServerRepository
 ) : ViewModel() {
   val url = MutableStateFlow("")
@@ -52,7 +53,7 @@ class AddServerViewModel @Inject constructor(
     id: ServerId,
     url: String
   ): Server? = try {
-    val api = MeteApiV1Factory.newInstance(url)
+    val api = factory.newInstance(url)
     val manifest = api.getManifest()
     val icon = manifest?.findBestIcon()
     Server(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
similarity index 77%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
index ed56a75c8623e0e0dca5d6d33c6ef73bfe48d6b4..5b5c6682244098d71a4154223e682a1b4b8ed50b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.transactions
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AttachMoney
 import androidx.compose.material.icons.filled.QuestionMark
@@ -45,32 +44,33 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.Purchase
+import de.chaosdorf.meteroid.model.Transaction
+import de.chaosdorf.meteroid.ui.PriceBadge
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toJavaLocalDateTime
 import kotlinx.datetime.toLocalDateTime
+import java.math.BigDecimal
 import java.time.format.DateTimeFormatter
 import java.time.format.FormatStyle
 
 @Composable
-fun PurchaseListItem(
-  purchase: Purchase,
+fun TransactionListItem(
+  transaction: Transaction,
   drink: Drink?
 ) {
-  val timestamp = purchase.createdAt.toLocalDateTime(TimeZone.currentSystemDefault())
+  val timestamp = transaction.timestamp.toLocalDateTime(TimeZone.currentSystemDefault())
   val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
 
   ListItem(
     headlineContent = {
-      val label =
-        if (drink != null) drink.name
-        else if (purchase.difference > 0.0) "Deposit"
-        else "Unknown"
+      val label = when {
+        drink != null -> drink.name
+        transaction.difference > BigDecimal.ZERO -> "Deposit"
+        else -> "Unknown"
+      }
       Text(label)
     },
     supportingContent = {
@@ -101,7 +101,7 @@ fun PurchaseListItem(
               .align(Alignment.Center)
               .fillMaxSize()
           )
-        } else if (purchase.difference > 0) {
+        } else if (transaction.difference > BigDecimal.ZERO) {
           Icon(
             Icons.Default.AttachMoney,
             contentDescription = null,
@@ -117,22 +117,9 @@ fun PurchaseListItem(
       }
     },
     trailingContent = {
-      val (foreground, background) =
-        if (purchase.difference < 0)
-          Pair(MaterialTheme.colorScheme.onError, MaterialTheme.colorScheme.error)
-        else
-          Pair(MaterialTheme.colorScheme.onPrimary, MaterialTheme.colorScheme.primary)
-
-      Text(
-        String.format("%.02f €", purchase.difference),
-        color = foreground,
-        fontSize = 14.sp,
-        lineHeight = 20.sp,
-        fontWeight = FontWeight.SemiBold,
-        modifier = Modifier
-          .clip(RoundedCornerShape(16.dp))
-          .background(background)
-          .padding(horizontal = 8.dp)
+      PriceBadge(
+        transaction.difference,
+        modifier = Modifier.padding(horizontal = 8.dp)
       )
     }
   )
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
similarity index 77%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
index ac31c251b4430f1931cebf1154a7eb811019f6bb..d6ceedbf2aa979303fb700761e513fc192a9a620 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
@@ -22,33 +22,31 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.Transactions
 
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.Scaffold
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.navigation.NavOptions
-import de.chaosdorf.meteroid.sync.SyncHandler
 import de.chaosdorf.meteroid.ui.navigation.HomeSections
 import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
 import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
+import de.chaosdorf.meteroid.ui.transactions.TransactionListItem
+import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel
 
 @Composable
-fun PurchaseListScreen(
-  viewModel: PurchaseViewModel,
+fun TransactionListScreen(
+  viewModel: TransactionViewModel,
   onNavigate: (String, NavOptions) -> Unit
 ) {
   val account by viewModel.account.collectAsState()
-  val purchases by viewModel.purchases.collectAsState()
-  val syncState by viewModel.syncState.collectAsState()
+  val transactions by viewModel.transactions.collectAsState()
 
   Scaffold(
     topBar = { MeteroidTopBar(account, onNavigate) },
@@ -60,14 +58,9 @@ fun PurchaseListScreen(
       )
     }
   ) { paddingValues: PaddingValues ->
-    Column {
-      if (syncState == SyncHandler.State.Loading) {
-        LinearProgressIndicator()
-      }
-      LazyColumn(modifier = Modifier.padding(paddingValues)) {
-        items(purchases) { (purchase, drink) ->
-          PurchaseListItem(purchase, drink)
-        }
+    LazyColumn(contentPadding = paddingValues) {
+      items(transactions) { (transaction, drink) ->
+        TransactionListItem(transaction, drink)
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
similarity index 70%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
index b31def9bf795ae8b62529c0c4680f752af141976..8c053ee161ae6d11ec671834086164a549df2494 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
@@ -22,17 +22,17 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.transactions
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.PurchaseRepository
+import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.sync.AccountProvider
-import de.chaosdorf.meteroid.sync.PurchaseSyncHandler
 import de.chaosdorf.meteroid.sync.SyncHandler
+import de.chaosdorf.meteroid.sync.TransactionSyncHandler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
@@ -40,31 +40,31 @@ import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
+import java.math.BigDecimal
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.minutes
 
 @HiltViewModel
-class PurchaseViewModel @Inject constructor(
+class TransactionViewModel @Inject constructor(
   accountProvider: AccountProvider,
-  repository: PurchaseRepository,
-  drinkRepository: DrinkRepository,
-  syncHandler: PurchaseSyncHandler
+  repository: TransactionRepository,
+  drinkRepository: DrinkRepository
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
-  val purchases: StateFlow<List<PurchaseInfo>> = accountProvider.account
+  val transactions: StateFlow<List<TransactionInfo>> = accountProvider.account
     .flatMapLatest { account ->
       account?.let { (server, maybeUser) ->
         maybeUser?.let { user ->
           combine(
             repository.getAllFlow(server.serverId, user.userId),
             drinkRepository.getAllFlow(server.serverId)
-          ) { purchases, drinks ->
-            purchases.map { purchase ->
-              PurchaseInfo(
-                purchase,
-                drinks.firstOrNull { drink -> drink.drinkId == purchase.drinkId }
+          ) { transactions, drinks ->
+            transactions.map { transaction ->
+              TransactionInfo(
+                transaction,
+                drinks.firstOrNull { drink -> drink.drinkId == transaction.drinkId }
               )
             }
           }
@@ -72,28 +72,26 @@ class PurchaseViewModel @Inject constructor(
       } ?: flowOf(emptyList())
     }.mapLatest { list ->
       list.mergeAdjecentDeposits()
-        .filter { it.drink != null || it.purchase.difference != 0.0 }
+        .filter { it.drink != null || it.transaction.difference != BigDecimal.ZERO }
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
-
-  val syncState: StateFlow<SyncHandler.State> = syncHandler.state
 }
 
-fun List<PurchaseInfo>.mergeAdjecentDeposits(): List<PurchaseInfo> {
-  val result = mutableListOf<PurchaseInfo>()
+fun List<TransactionInfo>.mergeAdjecentDeposits(): List<TransactionInfo> {
+  val result = mutableListOf<TransactionInfo>()
   for (entry in this) {
     val previous = result.lastOrNull()
     if (previous != null
-      && previous.purchase.difference > 0
-      && entry.purchase.difference > 0
+      && previous.transaction.difference > BigDecimal.ZERO
+      && entry.transaction.difference > BigDecimal.ZERO
       && previous.drink == null
       && entry.drink == null
-      && entry.purchase.createdAt.minus(previous.purchase.createdAt) < 5.minutes
+      && entry.transaction.timestamp.minus(previous.transaction.timestamp) < 5.minutes
     ) {
       result.removeLast()
       result.add(
         entry.copy(
-          purchase = entry.purchase.copy(
-            difference = entry.purchase.difference + previous.purchase.difference
+          transaction = entry.transaction.copy(
+            difference = entry.transaction.difference + previous.transaction.difference
           )
         )
       )
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
similarity index 88%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
index b389efebf5249dae8238e488e42b24749469e09a..c4654ce50a48a99930da29c83ba6431a7c78d504 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
@@ -22,12 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.transactions
 
 import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.Purchase
+import de.chaosdorf.meteroid.model.Transaction
 
-data class PurchaseInfo(
-    val purchase: Purchase,
+data class TransactionInfo(
+    val transaction: Transaction,
     val drink: Drink?
 )
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
index 02b12293813fd0cb1ca876516ce480d8f47e6ab8..a6890ee323a8d65496333ce696ced87a530c2699 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
@@ -27,13 +27,11 @@ package de.chaosdorf.meteroid.ui.users
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.ListItem
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
@@ -49,8 +47,7 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.dp
 import coil.compose.AsyncImage
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.meteroid.sync.SyncHandler
+import de.chaosdorf.mete.model.UserId
 
 @Composable
 fun UserListScreen(
@@ -61,7 +58,6 @@ fun UserListScreen(
 ) {
   val server by viewModel.account.collectAsState()
   val users by viewModel.users.collectAsState()
-  val syncState by viewModel.syncState.collectAsState()
 
   Scaffold(
     topBar = {
@@ -90,24 +86,19 @@ fun UserListScreen(
       )
     }
   ) { paddingValues ->
-    Column {
-      if (syncState == SyncHandler.State.Loading) {
-        LinearProgressIndicator()
+    LazyColumn(modifier = Modifier.padding(paddingValues)) {
+      items(users) { user ->
+        ListItem(
+          headlineContent = { Text(user.name) },
+          supportingContent = { Text(user.email) },
+          modifier = Modifier.clickable { onSelect(user.userId) }
+        )
       }
-      LazyColumn(modifier = Modifier.padding(paddingValues)) {
-        items(users) { user ->
-          ListItem(
-            headlineContent = { Text(user.name) },
-            supportingContent = { Text(user.email) },
-            modifier = Modifier.clickable { onSelect(user.userId) }
-          )
-        }
-        item {
-          ListItem(
-            headlineContent = { Text("Add User") },
-            modifier = Modifier.clickable { onAdd() }
-          )
-        }
+      item {
+        ListItem(
+          headlineContent = { Text("Add User") },
+          modifier = Modifier.clickable { onAdd() }
+        )
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
index bc36b148b7208947c54043e425431fdb9d9443fe..74029fd8617672231a6a33a36615c34b30941317 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
@@ -28,11 +28,9 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
-import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.model.UserRepository
 import de.chaosdorf.meteroid.sync.AccountProvider
-import de.chaosdorf.meteroid.sync.SyncHandler
 import de.chaosdorf.meteroid.sync.UserSyncHandler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -44,8 +42,7 @@ import javax.inject.Inject
 @HiltViewModel
 class UserListViewModel @Inject constructor(
   accountProvider: AccountProvider,
-  repository: UserRepository,
-  syncHandler: UserSyncHandler
+  repository: UserRepository
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@@ -56,6 +53,4 @@ class UserListViewModel @Inject constructor(
         repository.getAllFlow(server.serverId)
       } ?: flowOf(emptyList())
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
-
-  val syncState: StateFlow<SyncHandler.State> = syncHandler.state
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
index cd7ecd4dc8fddc4a5adc7f90df3ed638fea08372..65416dbf694bbcba2b3dfae7e1c48af95aa2d147 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
@@ -24,8 +24,8 @@
 
 package de.chaosdorf.meteroid.util
 
-import de.chaosdorf.mete.PwaIcon
-import de.chaosdorf.mete.PwaManifest
+import de.chaosdorf.mete.model.PwaIcon
+import de.chaosdorf.mete.model.PwaManifest
 import okhttp3.HttpUrl.Companion.toHttpUrl
 
 fun PwaManifest.findBestIcon(): PwaIcon? = icons.maxByOrNull {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f3578153c03f72e8598b58ce6260b6257e6913f5
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt
@@ -0,0 +1,35 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.lifecycle.SavedStateHandle
+
+fun <T> SavedStateHandle.update(key: String, block: (T?) -> T) {
+  this[key] = block(this[key])
+}
+
+fun <T> SavedStateHandle.update(key: String, default: T, block: (T) -> T) {
+  this[key] = block(this[key] ?: default)
+}
diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
index b8a0cbb9f2075143fb44088bef045f59af22274f..9efb6d66778c527a3cf0e91a61123aa799eca474 100644
--- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
+++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
@@ -2,11 +2,11 @@
   "formatVersion": 1,
   "database": {
     "version": 1,
-    "identityHash": "957bb839c1c422a256c594595437ee45",
+    "identityHash": "7aca7cdc33cbadb81643b32c5838f2a9",
     "entities": [
       {
         "tableName": "Drink",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` TEXT NOT NULL, `caffeine` INTEGER, `price` TEXT NOT NULL, `logoUrl` TEXT NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
         "fields": [
           {
             "fieldPath": "serverId",
@@ -35,7 +35,7 @@
           {
             "fieldPath": "volume",
             "columnName": "volume",
-            "affinity": "REAL",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
@@ -47,19 +47,7 @@
           {
             "fieldPath": "price",
             "columnName": "price",
-            "affinity": "REAL",
-            "notNull": true
-          },
-          {
-            "fieldPath": "createdAt",
-            "columnName": "createdAt",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "updatedAt",
-            "columnName": "updatedAt",
-            "affinity": "INTEGER",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
@@ -67,30 +55,6 @@
             "columnName": "logoUrl",
             "affinity": "TEXT",
             "notNull": true
-          },
-          {
-            "fieldPath": "logoFileName",
-            "columnName": "logoFileName",
-            "affinity": "TEXT",
-            "notNull": true
-          },
-          {
-            "fieldPath": "logoContentType",
-            "columnName": "logoContentType",
-            "affinity": "TEXT",
-            "notNull": true
-          },
-          {
-            "fieldPath": "logoFileSize",
-            "columnName": "logoFileSize",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "logoUpdatedAt",
-            "columnName": "logoUpdatedAt",
-            "affinity": "INTEGER",
-            "notNull": true
           }
         ],
         "primaryKey": {
@@ -155,7 +119,7 @@
       },
       {
         "tableName": "User",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` REAL NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` TEXT NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
         "fields": [
           {
             "fieldPath": "serverId",
@@ -190,7 +154,7 @@
           {
             "fieldPath": "balance",
             "columnName": "balance",
-            "affinity": "REAL",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
@@ -204,18 +168,6 @@
             "columnName": "redirect",
             "affinity": "INTEGER",
             "notNull": true
-          },
-          {
-            "fieldPath": "createdAt",
-            "columnName": "createdAt",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "updatedAt",
-            "columnName": "updatedAt",
-            "affinity": "INTEGER",
-            "notNull": true
           }
         ],
         "primaryKey": {
@@ -241,8 +193,8 @@
         ]
       },
       {
-        "tableName": "Purchase",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `purchaseId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` REAL NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `purchaseId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+        "tableName": "Transaction",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `transactionId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `transactionId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
         "fields": [
           {
             "fieldPath": "serverId",
@@ -251,8 +203,8 @@
             "notNull": true
           },
           {
-            "fieldPath": "purchaseId",
-            "columnName": "purchaseId",
+            "fieldPath": "transactionId",
+            "columnName": "transactionId",
             "affinity": "INTEGER",
             "notNull": true
           },
@@ -271,12 +223,12 @@
           {
             "fieldPath": "difference",
             "columnName": "difference",
-            "affinity": "REAL",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
-            "fieldPath": "createdAt",
-            "columnName": "createdAt",
+            "fieldPath": "timestamp",
+            "columnName": "timestamp",
             "affinity": "INTEGER",
             "notNull": true
           }
@@ -285,29 +237,29 @@
           "autoGenerate": false,
           "columnNames": [
             "serverId",
-            "purchaseId"
+            "transactionId"
           ]
         },
         "indices": [
           {
-            "name": "index_Purchase_serverId_userId",
+            "name": "index_Transaction_serverId_userId",
             "unique": false,
             "columnNames": [
               "serverId",
               "userId"
             ],
             "orders": [],
-            "createSql": "CREATE INDEX IF NOT EXISTS `index_Purchase_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)"
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)"
           },
           {
-            "name": "index_Purchase_serverId_drinkId",
+            "name": "index_Transaction_serverId_drinkId",
             "unique": false,
             "columnNames": [
               "serverId",
               "drinkId"
             ],
             "orders": [],
-            "createSql": "CREATE INDEX IF NOT EXISTS `index_Purchase_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)"
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)"
           }
         ],
         "foreignKeys": [
@@ -354,7 +306,7 @@
     "views": [],
     "setupQueries": [
       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '957bb839c1c422a256c594595437ee45')"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7aca7cdc33cbadb81643b32c5838f2a9')"
     ]
   }
 }
\ No newline at end of file
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
index a8b32864c10b8d73627ebc960cbe1809b224a17e..8cf5fac65c306c37e61882c2a458597da1c18c95 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
@@ -29,12 +29,13 @@ import androidx.room.RoomDatabase
 import androidx.room.TypeConverters
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.Purchase
-import de.chaosdorf.meteroid.model.PurchaseRepository
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerRepository
+import de.chaosdorf.meteroid.model.Transaction
+import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.model.UserRepository
+import de.chaosdorf.meteroid.util.BigDecimalTypeConverter
 import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter
 
 @Database(
@@ -43,14 +44,19 @@ import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter
     Drink::class,
     Server::class,
     User::class,
-    Purchase::class
+    Transaction::class
   ],
   autoMigrations = [],
 )
-@TypeConverters(value = [KotlinDatetimeTypeConverter::class])
+@TypeConverters(
+  value = [
+    KotlinDatetimeTypeConverter::class,
+    BigDecimalTypeConverter::class
+  ]
+)
 abstract class MeteroidDatabase : RoomDatabase() {
   abstract fun drinks(): DrinkRepository
   abstract fun server(): ServerRepository
   abstract fun users(): UserRepository
-  abstract fun purchases(): PurchaseRepository
+  abstract fun transactions(): TransactionRepository
 }
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
index ade8587945246de5bcbb0d1adf2a93e3933e6cc2..6ea48860e6b2ff82b43e85f391aff1db39e75022 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
@@ -30,10 +30,10 @@ import androidx.room.ForeignKey
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.v1.DrinkModelV1
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.DrinkModel
 import kotlinx.coroutines.flow.Flow
-import kotlinx.datetime.Instant
+import java.math.BigDecimal
 import java.net.URI
 
 @Entity(
@@ -47,33 +47,21 @@ data class Drink(
   val drinkId: DrinkId,
   val active: Boolean,
   val name: String,
-  val volume: Double,
+  val volume: BigDecimal,
   val caffeine: Int?,
-  val price: Double,
-  val createdAt: Instant,
-  val updatedAt: Instant,
+  val price: BigDecimal,
   val logoUrl: String,
-  val logoFileName: String,
-  val logoContentType: String,
-  val logoFileSize: Long,
-  val logoUpdatedAt: Instant
 ) {
   companion object {
-    fun fromModelV1(server: Server, value: DrinkModelV1) = Drink(
+    fun fromModel(server: Server, value: DrinkModel) = Drink(
       server.serverId,
-      value.id,
+      value.drinkId,
       value.active,
       value.name,
-      value.bottleSize,
+      value.volume,
       value.caffeine,
       value.price,
-      value.createdAt,
-      value.updatedAt,
-      URI.create(server.url).resolve(value.logoUrl).toString(),
-      value.logoFileName,
-      value.logoContentType,
-      value.logoFileSize,
-      value.logoUpdatedAt
+      URI.create(server.url).resolve(value.logoUrl).toString()
     )
   }
 }
@@ -89,7 +77,7 @@ interface DrinkRepository {
   @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY NAME ASC")
   suspend fun getAll(serverId: ServerId): List<Drink>
 
-  @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY NAME ASC")
+  @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY ACTIVE DESC, NAME ASC")
   fun getAllFlow(serverId: ServerId): Flow<List<Drink>>
 
   @Insert(onConflict = OnConflictStrategy.REPLACE)
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
similarity index 57%
rename from persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt
rename to persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
index fad3d86e350b4c7afd02239cdb84c17a82577a9f..c564bbdfb01c9543f815cd373b9384e64d1af3e1 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
@@ -31,62 +31,73 @@ import androidx.room.Index
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
-import de.chaosdorf.mete.AuditEntryId
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.AuditEntryModelV1
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.TransactionId
+import de.chaosdorf.mete.model.TransactionModel
+import de.chaosdorf.mete.model.UserId
 import kotlinx.coroutines.flow.Flow
 import kotlinx.datetime.Instant
+import java.math.BigDecimal
 
 @Entity(
-  primaryKeys = ["serverId", "purchaseId"],
+  primaryKeys = ["serverId", "transactionId"],
   foreignKeys = [
     ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE),
-    ForeignKey(User::class, ["serverId", "userId"], ["serverId", "userId"], onDelete = ForeignKey.CASCADE),
-    ForeignKey(Drink::class, ["serverId", "drinkId"], ["serverId", "drinkId"], onDelete = ForeignKey.NO_ACTION)
+    ForeignKey(
+      User::class,
+      ["serverId", "userId"],
+      ["serverId", "userId"],
+      onDelete = ForeignKey.CASCADE
+    ),
+    ForeignKey(
+      Drink::class,
+      ["serverId", "drinkId"],
+      ["serverId", "drinkId"],
+      onDelete = ForeignKey.NO_ACTION
+    )
   ],
   indices = [
     Index("serverId", "userId"),
     Index("serverId", "drinkId")
   ]
 )
-data class Purchase(
+data class Transaction(
   val serverId: ServerId,
-  val purchaseId: AuditEntryId,
+  val transactionId: TransactionId,
   val userId: UserId,
   val drinkId: DrinkId?,
-  val difference: Double,
-  val createdAt: Instant
+  val difference: BigDecimal,
+  val timestamp: Instant
 ) {
   companion object {
-    fun fromModelV1(server: Server, userId: UserId, value: AuditEntryModelV1) = Purchase(
+    fun fromModel(server: Server, userId: UserId, value: TransactionModel) = Transaction(
       server.serverId,
-      value.id,
+      value.transactionId,
       userId,
-      value.drink,
+      value.drinkId,
       value.difference,
-      value.createdAt
+      value.timestamp
     )
   }
 }
 
 @Dao
-interface PurchaseRepository {
-  @Query("SELECT * FROM Purchase WHERE serverId = :serverId AND userId = :userId ORDER BY createdAt DESC")
-  suspend fun getAll(serverId: ServerId, userId: UserId): List<Purchase>
+interface TransactionRepository {
+  @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC")
+  suspend fun getAll(serverId: ServerId, userId: UserId): List<Transaction>
 
-  @Query("SELECT * FROM Purchase WHERE serverId = :serverId AND userId = :userId ORDER BY createdAt DESC")
-  fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Purchase>>
+  @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC")
+  fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Transaction>>
 
   @Insert(onConflict = OnConflictStrategy.REPLACE)
-  suspend fun save(purchase: Purchase)
+  suspend fun save(transaction: Transaction)
 
-  @Query("DELETE FROM Purchase WHERE serverId = :serverId AND purchaseId = :purchaseId")
-  suspend fun delete(serverId: ServerId, purchaseId: AuditEntryId)
+  @Query("DELETE FROM `Transaction` WHERE serverId = :serverId AND transactionId = :transactionId")
+  suspend fun delete(serverId: ServerId, transactionId: TransactionId)
 
-  @Query("DELETE FROM Purchase WHERE serverId = :serverId AND userId = :userId")
+  @Query("DELETE FROM `Transaction` WHERE serverId = :serverId AND userId = :userId")
   suspend fun deleteAll(serverId: ServerId, userId: UserId)
 
-  @Query("DELETE FROM Purchase WHERE serverId = :serverId")
+  @Query("DELETE FROM `Transaction` WHERE serverId = :serverId")
   suspend fun deleteAll(serverId: ServerId)
 }
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
index 40d52aeec9d3096a47b95eab34a97349e02ae123..60561680db069844b9498cba1ef618947febd417 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
@@ -30,10 +30,10 @@ import androidx.room.ForeignKey
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.UserModelV1
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.mete.model.UserModel
 import kotlinx.coroutines.flow.Flow
-import kotlinx.datetime.Instant
+import java.math.BigDecimal
 import java.security.MessageDigest
 import java.util.Locale
 
@@ -49,24 +49,20 @@ data class User(
   val active: Boolean,
   val name: String,
   val email: String,
-  val balance: Double,
+  val balance: BigDecimal,
   val audit: Boolean,
   val redirect: Boolean,
-  val createdAt: Instant,
-  val updatedAt: Instant,
 ) {
   companion object {
-    fun fromModelV1(server: Server, value: UserModelV1) = User(
+    fun fromModel(server: Server, value: UserModel) = User(
       server.serverId,
-      value.id,
+      value.userId,
       value.active,
       value.name,
       value.email,
       value.balance,
       value.audit,
       value.redirect,
-      value.createdAt,
-      value.updatedAt
     )
   }
 
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4e46f64becdddc56a4dee9f27782facaa3fbe118
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt
@@ -0,0 +1,36 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.room.TypeConverter
+import java.math.BigDecimal
+
+class BigDecimalTypeConverter {
+  @TypeConverter
+  fun load(value: String): BigDecimal = BigDecimal(value)
+
+  @TypeConverter
+  fun store(value: BigDecimal): String = value.toString()
+}