Split shared expenses in a group — no account needed. See who owes whom and settle up.
io.github.kanylbullen/xupersplit MCP Server
This MCP server provides a toolset for splitting shared expenses in a group without requiring an account. It supports creating splits, collecting what participants paid, automatically calculating balances and who owes whom, and initiating one-tap payments from a balance view.
🛠️ Key Features
Split shared expenses in a group without an account
Automatically computes balances and “who-owes-whom”
Enables one-tap payments from the balance view
Includes an implementation context with 11 tools
🚀 Use Cases
Tracking shared expenses among a group
Viewing balances and settling up based on recorded payments
Supporting payment workflows using grouped expense data
⚡ Developer Benefits
Exposes functionality via an MCP server with 11 tools
Built around web and frontend technologies: Next.js, PWA, React, Tailwind CSS, TypeScript
Integrates backend/data context via Supabase
⚠️ Limitations
Described capabilities focus on expense splitting and settlement; no additional domains or account features are indicated
Create a new xupersplit for sharing expenses in a group. No account needed. Returns a secret link — give it to the user and tell them to share it with the group, since anyone holding the link can see and edit the split.
Parameters3
title
string
required
What the split is for, e.g. "Ski trip".
participants
array
required
Names of everyone splitting, at least two.
currency
string
optional
Currency code. Defaults to SEK.
Raw schema
{
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"description": "What the split is for, e.g. \"Ski trip\"."
},
"participants": {
"minItems": 2,
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"description": "Names of everyone splitting, at least two."
},
"currency": {
"description": "Currency code. Defaults to SEK.",
"type": "string",
"enum": [
"SEK",
"EUR",
"USD",
"NOK",
"DKK",
"ISK",
"GBP",
"CHF",
"PLN",
"THB",
"SATS"
]
}
},
"required": [
"title",
"participants"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
get_split
Read a split: participants, expenses, per-person balances and the shortest set of payments that settles everyone up.
Parameters1
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
}
},
"required": [
"split"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
add_expense
Record something one person paid for the group. Split equally by default. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters7
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
amount
number
required
Amount in the split's currency, as a decimal (e.g. 249.50).
paid_by
string
required
Who paid — a participant's name (or id) in this split.
description
string
optional
What it was, e.g. "Groceries".
split_between
array
optional
Names of the people sharing this cost, split equally. Defaults to everyone.
shares
array
optional
Uneven split. Either an exact `amount` per person (must add up to the total) or a relative `weight` per person (e.g. weight 2 for a couple). Don't combine with split_between.
date
string
optional
Date as YYYY-MM-DD. Defaults to today.
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
},
"amount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Amount in the split's currency, as a decimal (e.g. 249.50)."
},
"paid_by": {
"type": "string",
"description": "Who paid — a participant's name (or id) in this split."
},
"description": {
"description": "What it was, e.g. \"Groceries\".",
"type": "string"
},
"split_between": {
"description": "Names of the people sharing this cost, split equally. Defaults to everyone.",
"type": "array",
"items": {
"type": "string"
}
},
"shares": {
"description": "Uneven split. Either an exact `amount` per person (must add up to the total) or a relative `weight` per person (e.g. weight 2 for a couple). Don't combine with split_between.",
"type": "array",
"items": {
"type": "object",
"properties": {
"participant": {
"type": "string"
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"weight": {
"type": "number",
"exclusiveMinimum": 0
}
},
"required": [
"participant"
]
}
},
"date": {
"description": "Date as YYYY-MM-DD. Defaults to today.",
"type": "string"
}
},
"required": [
"split",
"amount",
"paid_by"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
record_payment
Record that one person paid another back, settling part or all of a debt. Use get_split first to see who should pay whom. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters6
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
from
string
required
Who sent the money — a participant's name (or id) in this split.
to
string
required
Who received it — a participant's name (or id) in this split.
amount
number
required
Amount in the split's currency, as a decimal (e.g. 249.50).
description
string
optional
How it was paid, e.g. "Swish, 3 March". Shown in the history.
date
string
optional
Date as YYYY-MM-DD. Defaults to today.
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
},
"from": {
"type": "string",
"description": "Who sent the money — a participant's name (or id) in this split."
},
"to": {
"type": "string",
"description": "Who received it — a participant's name (or id) in this split."
},
"amount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Amount in the split's currency, as a decimal (e.g. 249.50)."
},
"description": {
"description": "How it was paid, e.g. \"Swish, 3 March\". Shown in the history.",
"type": "string"
},
"date": {
"description": "Date as YYYY-MM-DD. Defaults to today.",
"type": "string"
}
},
"required": [
"split",
"from",
"to",
"amount"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
update_entry
Change an existing expense or payment. Only the fields you pass are changed. Entry ids come from get_split. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters9
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
entry_id
string
required
The entry's id, from get_split.
amount
number
optional
Amount in the split's currency, as a decimal (e.g. 249.50).
description
string
optional
paid_by
string
optional
Move the entry to a different payer.
to
string
optional
New recipient — payments only.
split_between
array
optional
Names of the people sharing this cost, split equally. Defaults to everyone.
shares
array
optional
Uneven split. Either an exact `amount` per person (must add up to the total) or a relative `weight` per person (e.g. weight 2 for a couple). Don't combine with split_between.
date
string
optional
Date as YYYY-MM-DD. Defaults to today.
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
},
"entry_id": {
"type": "string",
"description": "The entry's id, from get_split."
},
"amount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Amount in the split's currency, as a decimal (e.g. 249.50)."
},
"description": {
"type": "string"
},
"paid_by": {
"description": "Move the entry to a different payer.",
"type": "string"
},
"to": {
"description": "New recipient — payments only.",
"type": "string"
},
"split_between": {
"description": "Names of the people sharing this cost, split equally. Defaults to everyone.",
"type": "array",
"items": {
"type": "string"
}
},
"shares": {
"description": "Uneven split. Either an exact `amount` per person (must add up to the total) or a relative `weight` per person (e.g. weight 2 for a couple). Don't combine with split_between.",
"type": "array",
"items": {
"type": "object",
"properties": {
"participant": {
"type": "string"
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"weight": {
"type": "number",
"exclusiveMinimum": 0
}
},
"required": [
"participant"
]
}
},
"date": {
"description": "Date as YYYY-MM-DD. Defaults to today.",
"type": "string"
}
},
"required": [
"split",
"entry_id"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
delete_entry
Remove an expense or payment for good. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters2
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
entry_id
string
required
The entry's id, from get_split.
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
},
"entry_id": {
"type": "string",
"description": "The entry's id, from get_split."
}
},
"required": [
"split",
"entry_id"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
add_participant
Add someone to an existing split. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters2
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
Change a participant's name. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters3
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
participant
string
required
Who to rename — a participant's name (or id) in this split.
name
string
required
The new name.
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
},
"participant": {
"type": "string",
"description": "Who to rename — a participant's name (or id) in this split."
},
"name": {
"type": "string",
"minLength": 1,
"description": "The new name."
}
},
"required": [
"split",
"participant",
"name"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
remove_participant
Remove someone from a split. Only works if they aren't on any expense yet. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters2
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
participant
string
required
Who to remove — a participant's name (or id) in this split.
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
},
"participant": {
"type": "string",
"description": "Who to remove — a participant's name (or id) in this split."
}
},
"required": [
"split",
"participant"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
set_payment_methods
Set how a participant wants to be paid back, so the split can show a QR code or pay link. Replaces their current list (pass an empty array to clear it). Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters3
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
participant
string
required
Whose details these are — a participant's name (or id) in this split.
methods
array
required
Raw schema
{
"type": "object",
"properties": {
"split": {
"type": "string",
"description": "The split key, or the whole https://split.xuper.fun/k/<key> link."
},
"participant": {
"type": "string",
"description": "Whose details these are — a participant's name (or id) in this split."
},
"methods": {
"maxItems": 8,
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"swish",
"vipps",
"mobilepay",
"revolut",
"lightning",
"evm",
"solana",
"iban"
]
},
"value": {
"type": "string",
"description": "Phone number for swish/vipps/mobilepay, IBAN, Revolut tag, Lightning address, 0x/ENS address, or Solana address."
}
},
"required": [
"type",
"value"
]
}
}
},
"required": [
"split",
"participant",
"methods"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
update_split
Change a split's title or currency. The currency is locked once the split has expenses. Works on simple, accountless splits. Secure splits (created by a signed-in user) are read-only here and must be opened in a browser.
Parameters3
split
string
required
The split key, or the whole https://split.xuper.fun/k/<key> link.
Split shared expenses without the fuss. Create a split, share the link, and
let everyone add what they paid — balances and who-owes-whom are worked out
automatically, with one-tap payments straight from the balance view.
Xupersplit demo — create a split, add an expense, then settle up onchain in USDC
Works with AI agents. Xupersplit is an MCP server too —
point your assistant at one URL and it can create the split, add what everyone
paid and tell you who owes whom. No account, no API key, nothing to install.
code
claude mcp add --transport http xupersplit https://split.xuper.fun/api/mcp
What it is
A clean, accountless expense splitter. The secret link is the key — anyone
with it can add expenses and settle up; no login required. Optional email
sign-in just makes your splits follow you across devices. Built as a one-prompt
project and grown from there.
Features
No account needed. The unguessable split link (122 bits of entropy) is
the capability. Sign-in is optional.
Flexible splitting — equal, weighted shares, or exact amounts, with cent
rounding via the largest-remainder method.
Smart settlements — the minimum set of "A pays B" transfers, with
partial payments ("pay all or part") and strike-through once settled.
Don't settle too early — see who has opened the split and who's marked
themselves done; the pay dialog warns if someone hasn't weighed in yet.
Secure splits(optional, when signed in) — bind participants to their
accounts: you can only edit your own payment details and enter your own
expenses. The creator picks who must log in, who can view, and how people join.
Multi-currency — enter expenses in any currency with the rate locked at
save (Kittysplit-style); set a main currency per split. Includes sats —
run a whole split in bitcoin if you like.
Eight payment methods, several with genuine one-tap prefill — see below.
MCP server — AI agents can run a whole split over
Model Context Protocol, with no account and no API key.
Six languages — English, Svenska, Norsk, Dansk, Suomi, Íslenska
(auto-detected, switchable).
Privacy by design — payment details can be wiped once everyone is square,
inactive splits are purged after 6 months, IP hashes deleted within a day,
CSV/JSON export, full GDPR policy.
Dark / light / system theme, cookie-less analytics, discreet cookie notice.
Payments
Xupersplit stores each recipient's payment handle(s) and, wherever a payment
network exposes an open, agreement-free interface, turns the balance row
into a real one-tap payment — prefilled with the exact amount. No money ever
passes through Xupersplit; it only builds the link/invoice/transaction the payer
approves in their own app.
Method
Experience
How
Swish 🇸🇪
QR + app deep link, amount prefilled
Public app.swish.nu link + QR endpoint — no merchant contract
Lightning ⚡
QR + lightning: link, exact amount baked in
LNURL-pay (LUD-16): a lightning address → BOLT11 invoice
WalletConnect (Reown AppKit) — Phantom/Solflare, recipient ATA auto-created
Ethereum / Solana address
Address QR + copy, ENS resolved
0x… / name.eth / base58 — for any wallet
Revolut
Clickable revolut.me profile link
Opens the recipient's profile to pay
Vipps · MobilePay · IBAN
Stored handle + copy button
No open P2P API — the payer finishes in their own app
Why the difference? Swish exposes a genuinely open prefilled deep link and
QR endpoint; Lightning's LNURL and EVM/Solana over WalletConnect are open
protocols. Vipps
and MobilePay (now Vipps MobilePay) only offer amount-prefilled flows through
their merchant APIs — a business agreement that routes money to a company,
not person-to-person — so for those Xupersplit does the honest thing and shows
the handle with a copy button. If they ever ship an open P2P deep link, wiring
it in is a small change. PRs welcome. 🤞
Crypto is irreversible. Crypto methods show extra warnings, and any
method warns (with a date) if the recipient's details were ever changed from
what was first entered — anyone with the link can edit them.
MCP server
Xupersplit speaks Model Context Protocol, so
an AI assistant can create a split, add what everyone paid and report who owes
whom — then hand you the link to share. Accountless like the rest of the app:
no sign-up, no API key.
bash
claude mcp add --transport http xupersplit https://split.xuper.fun/api/mcp
Or point any MCP client at https://split.xuper.fun/api/mcp (Streamable HTTP;
stdio-only clients can bridge via npx mcp-remote). Self-hosted instances get
the same endpoint at their own /api/mcp.
It also works as a custom connector in Claude and ChatGPT on the web —
paste the endpoint, no authentication. ChatGPT needs developer mode switched on
first, or the option to add one isn't there at all. Step-by-step at
/mcp.
Eleven tools cover the whole lifecycle — create_split, get_split,
add_expense, record_payment, update_entry, delete_entry,
add_participant, rename_participant, remove_participant,
set_payment_methods, update_split. People are referred to by name and
amounts are plain decimals, so an agent never handles uuids or cents.
The server is a thin layer over the same RPCs and money.ts helpers the web app
uses, and it only ever holds the anonymous role — which is what keeps secure
splits (they need a signed-in auth.uid()) unreachable over MCP. Full details
at /mcp.
Architecture
No service-role key in the app. All data access goes through
security definer Postgres RPCs (split_data, save_entry,
set_payment_methods, …) where the secret split key in the URL is the
capability. RLS is deny-all on every table and direct grants are revoked — the
client only ever holds the public publishable key. Schema and every change
live in supabase/migrations/.
Next.js App Router + server actions; the client is plain React, no state
library. Tailwind v4 with CSS-variable theming.
Thin, keyless API routes proxy the open payment networks, all
same-origin-locked: /api/swish-qr, /api/ln-invoice (LNURL-pay),
/api/ens (viem), /api/fx (fiat + BTC, with provider fallback).
WalletConnect is fully gated on a project id — absent, the EVM dialog
cleanly falls back to QR + copy.
Privacy & abuse controls — settle-time payment wipe (opt-out), 6-month
purge of inactive splits, per-IP-hash + global create rate limits, and a
daily job that flags split-key enumeration attempts.
npm install
cp .env.example .env.local # add your own Supabase URL + anon key
npm run dev
.env.local:
code
NEXT_PUBLIC_SUPABASE_URL=https://<your-project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your publishable key>
# Optional — enables the WalletConnect USDC flow (free id from cloud.reown.com)
NEXT_PUBLIC_REOWN_PROJECT_ID=<your reown project id>
Deploy your own
Self-hostable on the free tiers of Supabase + Vercel.
Supabase — create a project (EU regions keep data in Europe), then apply
the schema with supabase link --project-ref <ref> && supabase db push
(runs every migration in supabase/migrations/).
Grab the Project URL and publishable (anon) key. For optional email
sign-in, configure SMTP and the …/auth/confirm redirect.
Vercel — import the repo, add NEXT_PUBLIC_SUPABASE_URL and
NEXT_PUBLIC_SUPABASE_ANON_KEY (both safe to expose; security relies on RLS
RPCs). Add NEXT_PUBLIC_REOWN_PROJECT_ID too if you want WalletConnect.
Deploy.
Custom domain (optional) — add it in Vercel, point a DNS-only CNAME
to cname.vercel-dns.com, and add the domain's …/auth/confirm to the
Supabase redirect allowlist if using email sign-in.
Full self-host with Docker
Prefer to own the whole stack? selfhost/ brings up the app and
its own backend — Postgres, auth (GoTrue), the REST/RPC layer (PostgREST) and
a local mailbox — with no external services. The app itself acts as the gateway,
so the browser only ever talks to one origin.
bash
cd selfhost
cp .env.example .env# ⚠️ change the secrets — see the notes in the file
docker compose up -d --build
HTTPS is one flag away: point a domain at the host, set DOMAIN +
ACME_EMAIL and SITE_URL=https://… in .env, then
docker compose --profile tls up -d --build — Caddy fetches and renews a
Let's Encrypt certificate automatically. Behind NAT or want a wildcard cert?
Set CF_API_TOKEN (a scoped Cloudflare token) and Caddy uses the DNS-01
challenge instead — no port 80 exposure needed.
The migrations in supabase/migrations/ are applied
automatically on first start.
.env.example ships with public demo JWT keys so it runs out of the box.
For anything internet-facing, change JWT_SECRET + the passwords and
regenerate ANON_KEY/SERVICE_ROLE_KEY (any JWT tool works — sign
{"role":"anon",...} / {"role":"service_role",...} with the new secret).
Set APP_PORT / MAILPIT_PORT in .env to change host ports;
REOWN_PROJECT_ID enables the WalletConnect pay buttons.
See selfhost/README.md for the full guide —
secret regeneration, the gateway architecture, Cloudflare DNS-01, a config
reference and troubleshooting.
Tests & CI
Playwright smoke tests (npm run test:e2e) run on every PR against a local
production build, gating merges to main; the pure split/balance/settlement
logic lives in src/lib/money.ts.
Contributing
Issues and PRs are welcome, and so is a bug report from someone who just used
the site. Start with CONTRIBUTING.md.
Never post a split link. A split's URL is its password — anyone who sees
it can read and edit that split, and issues are public and indexed. Describe
the problem, or make a throwaway split with fake names.
Most wanted right now: a real open P2P deep link for Vipps or MobilePay
so the amount can be prefilled the way Swish and Lightning already do, plus
additional payment rails and translation fixes across the six locales. The
codebase is small and fully typed.