diff --git a/prisma/migrations/20230305155931_add_money_transfers/migration.sql b/prisma/migrations/20230305155931_add_money_transfers/migration.sql
new file mode 100644
index 0000000000000000000000000000000000000000..efbc90b6852d22f99bbc2c4383fa6297c4b56db5
--- /dev/null
+++ b/prisma/migrations/20230305155931_add_money_transfers/migration.sql
@@ -0,0 +1,16 @@
+-- CreateTable
+CREATE TABLE "MoneyTransfer" (
+    "id" SERIAL NOT NULL,
+    "amount" INTEGER NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "fromId" INTEGER NOT NULL,
+    "toId" INTEGER NOT NULL,
+
+    CONSTRAINT "MoneyTransfer_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "MoneyTransfer" ADD CONSTRAINT "MoneyTransfer_fromId_fkey" FOREIGN KEY ("fromId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "MoneyTransfer" ADD CONSTRAINT "MoneyTransfer_toId_fkey" FOREIGN KEY ("toId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 488a449e2f894ba309167c404297d0c5b5aa2945..576094a20590fe4d23b5dd8bb150fcf8ac8fc1b0 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -20,6 +20,8 @@ model User {
   transactionsVerified Transaction[] @relation("verifiedBy")
   itemTransactions ItemTransaction[]
   cards     UserCard[]
+  moneyTransfersSent MoneyTransfer[] @relation("from")
+  moneyTransfersReceived MoneyTransfer[] @relation("to")
   updatedAt DateTime @updatedAt
   comment   String   @default("")
 }
@@ -86,3 +88,13 @@ model Restock {
   cost      Int
   createdAt DateTime @default(now())
 }
+
+model MoneyTransfer {
+  id        Int      @id @default(autoincrement())
+  amount    Int
+  createdAt DateTime @default(now())
+  from      User     @relation("from", fields: [fromId], references: [id])
+  fromId    Int
+  to        User     @relation("to", fields: [toId], references: [id])
+  toId      Int
+}
diff --git a/src/lib/customcodes.js b/src/lib/customcodes.js
index a228f55aa2937a928d3c09ed0dc4ed3991fb5b56..df64f710f4ffa586b6d65f03347c9fd37b56a46d 100644
--- a/src/lib/customcodes.js
+++ b/src/lib/customcodes.js
@@ -1,4 +1,5 @@
 export const Codes = {
 	Pfand: "PFAND",
-	Custom: "CUSTOM"
+	Custom: "CUSTOM",
+	MoneyTransfer: "TRANSFER"
 };
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 9ffb34b0f6488d6ffb1f04f1208ae02b46d8dab1..bfee8f6ab991e136b1e7bc59fd34a8384a4caae9 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,7 +1,7 @@
 <script>
 import { onDestroy, onMount } from "svelte";
 import { browser, dev } from "$app/environment";
-import { goto } from "$app/navigation";
+import { goto, invalidateAll } from "$app/navigation";
 import { addMessage, MessageType } from "$lib/messages";
 import { Codes } from "$lib/customcodes";
 import { addInputHandler, removeInputHandler } from "$lib/inputhandler";
@@ -12,15 +12,17 @@ import ItemList from "../components/ItemList.svelte";
 
 	export let data;
 	
-	const items = data.items || [];
-	const categories = data.categories || [];
-	const usercard = data.user.card;
+	$: items = data.items || [];
+	let categories = data.categories || [];
+	let usercard = data.user.card;
 	
 	let categoryElements = categories.map(category => ({name: category.name, element: null}));
 	let elementsInView = categories.map(() => false);
 	$: categoryInView = elementsInView.lastIndexOf(true);
 	
 	let cart = [];
+	// if string, then it is a code, else it is a custom item
+	$: cartItems = cart.map(code => typeof code === "string" ? items.find(item => item.code === code) : code);
 	function emptyCart(){
 		cart = [];
 		resetTimer();
@@ -79,7 +81,7 @@ import ItemList from "../components/ItemList.svelte";
 		if(cart.length === 0){
 			return logout();
 		}
-		const items = cart.map(item => ({code: item.code, price: item.price, premium: item.premium}));
+		const items = cartItems.map(item => ({code: item.code, price: item.price, premium: item.premium}));
 		fetch("/api/buy", {
 			method: "POST",
 			body: JSON.stringify({card: usercard, items}),
@@ -95,13 +97,29 @@ import ItemList from "../components/ItemList.svelte";
 		});
 	}
 	
+	function transferMoney(target, amount){
+		fetch("/api/transfer", {
+			method: "POST",
+			body: JSON.stringify({card: usercard, target, amount}),
+			headers: { "Content-Type": "application/json" }
+		}).then(r => r.json()).then(r => {
+			if(r.message){
+				addMessage(MessageType.ERROR, r.message);
+			} else {
+				// Überweisung erfolgreich
+				addMessage(MessageType.SUCCESS, "Überweisung erfolgreich");
+				invalidateAll();
+			}
+		});
+	}
+	
 	function handleCode(code){
 		if(specialCodes[code]){
 			specialCodes[code]();
 		}else {
 			const item = items.find(item => item.code === code);
 			if(item){
-				addToCart(item);
+				addToCart(item.code);
 			}else if(code === usercard){
 				buy();
 			}else if(code.startsWith("#" + Codes.Custom + "-")){
@@ -120,6 +138,15 @@ import ItemList from "../components/ItemList.svelte";
 					else
 						addMessage(MessageType.ERROR, "Der Preis muss größer als 0 sein.");
 				}
+			}else if(code.startsWith("#" + Codes.MoneyTransfer + "-")){
+				const parts = code.substring(2 + Codes.MoneyTransfer.length).split("-");
+				if(parts.length !== 2) return addMessage(MessageType.ERROR, "Ungültiger Code");
+				const to = parseInt(parts[0]);
+				const amount = parseInt(parts[1]);
+				if(!Number.isInteger(to) || !Number.isInteger(amount) || to < 0 || amount < 0) return addMessage(MessageType.ERROR, "Ungültiger Code");
+				if(amount > data.user.balance) return addMessage(MessageType.ERROR, "Nicht genügend Guthaben");
+				// TODO add confirmation
+				transferMoney(to, amount);
 			}else{
 				addMessage(MessageType.INFO, "Unbekannter Code");
 			}
@@ -213,13 +240,13 @@ import ItemList from "../components/ItemList.svelte";
 				{#each categories as category, i}
 					<h2 bind:this={categoryElements[i].element}>{category.name}</h2>
 					<div class="item-list">
-						<ItemList items={items.filter(item=>item.categoryId===category.id)} onClick={addToCart} priceModifier={x=>x} bind:inView={elementsInView[i]} />
+						<ItemList items={items.filter(item=>item.categoryId===category.id)} onClick={item=>addToCart(item.code)} priceModifier={x=>x} bind:inView={elementsInView[i]} />
 					</div>
 				{/each}
 			</div>
 		</div>
 		<div class="cart">
-			<Cart cart={cart} removeFromCart={removeItemAt} buy={buy} />
+			<Cart cart={cartItems} removeFromCart={removeItemAt} buy={buy} />
 		</div>
 	</div>
 </div>
diff --git a/src/routes/api/[slug]/+server.js b/src/routes/api/[slug]/+server.js
index f2d69ab7fcb2f7e24cd2e6f47e25a37cd56fc031..e4946d5ea92c2128c3cecf42ea3fa269fbe86268 100644
--- a/src/routes/api/[slug]/+server.js
+++ b/src/routes/api/[slug]/+server.js
@@ -1,8 +1,8 @@
 import { getPriceModifier } from "../../admin/api/articles";
 import { perms } from "$lib/perms";
 import { getArticles } from "../articles";
-import { buyArticles } from "../transactions";
-import { getUser } from "../users";
+import { buyArticles, transferMoney } from "../transactions";
+import { getUser, getUserById } from "../users";
 import { Codes } from "$lib/customcodes";
 
 
@@ -22,17 +22,33 @@ export async function POST(event) {
 			try{
 				const data = await event.request.json();
 				if(!data.card || !data.items) return new Response(JSON.stringify({message: "Missing required fields"}), {headers: {'content-type': 'application/json', 'status': 400}});
-				const user = await getUser(data.card);
+				const user = await getUser(data.card); // TODO probably switch to using event.locals.user
 				if(!user) return new Response(JSON.stringify({message: "User not found"}), {headers: {'content-type': 'application/json', 'status': 404}});
 				const articles = (await getArticles()).reduce((acc, item) => {acc[item.code] = item; return acc;}, {});
 				const priceModifier = getPriceModifier(user.balance, !!(user.perms & perms.NO_PREMIUM));
 				if(!isValidCart(data.items, articles, priceModifier)) return new Response(JSON.stringify({message: "Invalid cart"}), {headers: {'content-type': 'application/json', 'status': 400}});
-				const res = buyArticles(user.id, data.card, data.items, !!(user.perms & perms.NO_BALANCE));
+				const res = await buyArticles(user.id, data.card, data.items, !!(user.perms & perms.NO_BALANCE));
 				return new Response(JSON.stringify(res), {headers: {'content-type': 'application/json', 'status': 200}});
 			}catch(e){
 				return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}});
 			}
 		}
+		case "transfer": {
+			try{
+				const data = await event.request.json();
+				if(!data.card || !Number.isInteger(data.amount) || data.amount <= 0 || !data.target) return new Response(JSON.stringify({message: "Missing required fields"}), {headers: {'content-type': 'application/json', 'status': 400}});
+				const user = await getUser(data.card); // TODO probably switch to using event.locals.user
+				if(!user) return new Response(JSON.stringify({message: "User not found"}), {headers: {'content-type': 'application/json', 'status': 404}});
+				if(user.balance < data.amount) return new Response(JSON.stringify({message: "Not enough balance"}), {headers: {'content-type': 'application/json', 'status': 400}});
+				const target = await getUserById(data.target);
+				if(!target) return new Response(JSON.stringify({message: "Target not found"}), {headers: {'content-type': 'application/json', 'status': 404}});
+				const { from } = await transferMoney(user.id, target.id, data.amount);
+				// don't return the to-user, because it contains the target's information
+				return new Response(JSON.stringify(from), {headers: {'content-type': 'application/json', 'status': 200}});
+			}catch(e){
+				return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}});
+			}
+		}
 		default:
 			return new Response("Unknown endpoint", {status: 400});
 	}
diff --git a/src/routes/api/transactions.js b/src/routes/api/transactions.js
index 442cf4212931063225cb54625ae270ddecf0b0c5..ee1d042977a2e292cd07d12679c2cfe3acac2309 100644
--- a/src/routes/api/transactions.js
+++ b/src/routes/api/transactions.js
@@ -28,3 +28,31 @@ export async function buyArticles(userId, card, items, noBalance=false) {
 	]);
 	return { user, itemTransactions };
 }
+
+export async function transferMoney(fromId, toId, amount){
+	const [to, from] = await db.$transaction([
+		db.user.update({
+			where: { id: toId },
+			data: {
+				balance: {
+					increment: amount
+				},
+				moneyTransfersReceived: {
+					create: {
+						amount,
+						from: { connect: { id: fromId } }
+					}
+				}
+			}
+		}),
+		db.user.update({
+			where: { id: fromId },
+			data: {
+				balance: {
+					decrement: amount
+				}
+			}
+		})
+	]);
+	return { to, from };
+}