Product documentation

Everything users need to understand Splitsy.

Splitsy turns shared bills into trackable USDC payment flows. It scans receipts, calculates who owes what, records debts on Arc Testnet, lets payers fund and pay from their wallets, and automates recurring collection when a cycle becomes due.

Overview

Splitsy is built for groups that need more than a calculator screenshot. A splitter can upload a receipt, review the extracted bill, assign payer wallets, and create an onchain record of each participant's share. Payers can connect their wallets later, see only their own debts, pay in full or partially, and bridge USDC into Arc when their balance lives on another supported testnet.

The application has two product surfaces. Bills are one-time debts linked to a receipt or expense.Recurring tabs are scheduled payment agreements, such as rent, subscriptions, shared services, or repeating household costs. Both flows are designed around explicit wallet approval and visible balances.

Scan and review

Upload a bill image, parse merchant totals and line items, convert non-USD totals to USD, and verify the split before anything is submitted.

Wallet-first payment

Browser wallets sign contract transactions. The app uses EIP-1193/EIP-6963 provider discovery and Viem wallet clients for Arc Testnet interactions.

Pay cross-chain with Gateway

Payers with USDC on Avalanche, Base, or Ethereum can pay directly from those chains to Arc Testnet. Gateway burns on the source chain, fetches an attestation, and mints on Arc — all in one two-step flow with no bridge UI.

Automated recurring settlement

Once a payer has approved a recurring tab, Splitsy checks due cycles automatically so users do not manually press a settlement button every cycle.

An agent that pays for you

Bills raised against you can be settled by your own funded agent, under ceilings you set. It spends only the USDC you send it, and every settlement is a public on-chain job a second agent has to sign off.

Using Splitsy

1

Connect or upload

Start in the Bills tab. Upload a receipt image or review the default bill fields. The scanner reads totals, tax, tip, line items, and confidence notes.

2

Review the bill

Confirm the merchant, currency, subtotal, tax, tip, total, and line items. Non-USD bills are quoted into USD for payment calculations.

3

Choose a split

Use equal split for a quick division or manual split when participants owe different amounts. Each payer needs a valid EVM wallet address and a positive amount.

4

Submit the bill

The splitter creates a bill in the BillSplitRegistry contract. The contract stores a metadata hash, participant addresses, and each participant's owed USDC amount.

5

Payers settle

Payers connect the matching wallet, approve the registry for the selected USDC amount, and call the payment flow. Payments can be partial as long as they do not exceed the remaining debt.

6

Splitter claims

Paid funds accumulate as claimable balance in the registry. The splitter can claim any amount up to the available paid balance.

Sign-In and Wallets

Splitsy lets you split a bill with anyone by their handle or email — even before they have ever opened the app. You sign in with X, Discord, Google, or a one-time email code, and each method gives you a ready-to-use USDC wallet on Arc Testnet, so a debtor never has to install a browser wallet, hold a seed phrase, or understand gas to pay what they owe. This section explains exactly what data is used, how the wallet is created, and why Splitsy makes the choices it does.

Four ways to sign in

Choose X, Discord, Google, or email. X and Discord use OAuth 2.0 and read only your public profile (id, username, name, avatar). Google returns your verified email, name, and picture. Email sends a 6-digit one-time code. No method lets Splitsy post on your behalf or read your messages.

Minimal, identify-only access

For X the scopes are tweet.read, users.read, offline.access; for Discord,identify — no write access, no email. Google and email sign-in identify you by your email address. You can revoke an OAuth provider anytime from its connected-apps settings.

A real wallet for your identity

On first sign-in, Splitsy creates a Circle developer-controlled wallet on Arc Testnet keyed to your provider identity. It is a genuine on-chain account with its own address — you can receive USDC to it, send from it, and view it on the block explorer.

A PIN before money moves

Sending USDC requires a wallet PIN you set yourself. Entering it unlocks sends for five minutes, then re-locks. The PIN is stored only as a salted scrypt hash; the raw PIN never leaves your device in readable form and is never stored.

How the identity flow works

1

Authorize with your provider

For X, Discord, and Google you're redirected to that provider's consent screen using OAuth 2.0 with PKCE. Splitsy's server holds the client secret; a signed state value and PKCE code verifier prevent request forgery and code interception. Email sign-in instead emails you a single-use 6-digit code.

2

Read the basic profile once

After you approve, Splitsy makes a single call to read your id/handle (or verified email), name, and avatar. No further data is requested from the provider.

3

Create or reuse your wallet

Splitsy provisions a Circle wallet keyed to your provider identity (idempotently — the same identity always maps to the same wallet), then stores your handle/email, avatar, and wallet address so friends can tag you.

4

Set a session

A signed, http-only session cookie keeps you logged in. It stores only your Splitsy user id — no tokens or profile data are exposed to the browser.

5

Discover what you owe

Any bill already tagged to your handle or email is linked to you on sign-in and appears under your unpaid bills, ready to pay from your wallet.

Why a developer-controlled wallet (and not a user-controlled one)

Circle offers two wallet models. A user-controlled walletis non-custodial but requires the user to authenticate to Circle directly — via Google, Apple, Facebook, email OTP, or a PIN — because the user holds a key share. Some of Splitsy's sign-in methods (like X and Discord) are not supported Circle logins at all, so they cannot unlock a user-controlled wallet; and for the ones that overlap, bridging the two would still force every debtor through a second, unrelated login (and a recovery-phrase burden) just to pay a dinner split — the exact friction Splitsy exists to remove.

A developer-controlled wallet is created and operated server-side, keyed to a reference id (your provider identity). That lets Splitsy give anyone a working USDC wallet the instant they sign in — under a single, uniform model across all four providers, with no extra login, no seed phrase, and no app to install. Because Splitsy runs on Arc Testnet with test USDC that has no monetary value, the custodial trade-off carries no financial risk while delivering the smoothest possible onboarding. A future mainnet deployment would revisit this and offer genuine self-custody for real funds.

PropertyDeveloper-controlled (Splitsy today)User-controlled
Works from a handle/email aloneYes — created server-side on sign-inOnly for Circle-supported logins (not X/Discord)
Onboarding steps for a newcomerNone beyond signing inSecond login plus recovery-phrase setup
CustodyServer-operated (testnet, valueless USDC)User holds a key share
NetworkArc Testnet (EOA/SCA), USDC transfersArc Testnet

Off-chain ledger for handle-tagged bills

The BillSplitRegistrycontract records debts by wallet address and needs every participant's address at creation time. A handle or email you tag may belong to someone who has not signed in yet and therefore has no address, so tagged bills are not written to the registry. Instead they live in an off-chain ledger: the bill and each debtor's share are stored keyed by provider + handle/email, and are linked to a real wallet the moment that person signs in. This is a deliberate second mode alongside the on-chain registry, chosen so you can split with anyone without knowing their address.

Direct settlement

To pay, your wallet sends USDC directly to the creditor's wallet on Arc — no escrow contract in the middle. Splitsy initiates the transfer, confirms it, and marks the debt paid. Paid bills move to the history at the foot of your Dashboard with an explorer link.

Send, receive, and history

Your wallet widget shows your live USDC balance, a copyable receive address, a PIN-gated send form, and a transaction history read from Circle — each with a link to the Arc block explorer.

Bill Splits

The one-time bill flow is anchored by BillSplitRegistry. The registry does not need to know the full receipt body; it stores a hash of bill metadata plus the participant list and amounts. This keeps the contract focused on debt accounting while leaving rich receipt display to the app.

ActionContract functionWhat happens
Create billcreateBill(bytes32,address[],uint256[])Registers participant debts and emits BillCreated.
Pay debtpayDebt(uint256,uint256)Transfers USDC from payer to the registry and updates paid totals.
Claim fundsclaim(uint256,uint256)Allows only the splitter to withdraw paid, unclaimed funds.
Look up debtsbillIdsForParticipant, getParticipantLoads debts for the connected payer wallet.

Amounts are represented with 6 decimals to match USDC. User-entered dollar values are converted into USDC base units before they are submitted to the contract.

Bill Verification

Every on-chain bill carries a verification badge in the payer's view. It answers two different questions, and the whole design hinges on keeping them separate:

1. Is this a genuine bill?

Are the merchant, total, and split shown to you exactly what the creator committed to Arc — with nothing changed since? This is about authenticity, and it is proven by cryptography.

2. Is the total correct?

Does the amount you're being charged actually match the receipt? This is about honesty, and it is checked by re-reading the receipt image itself.

What actually goes on-chain

Storing a full receipt on a blockchain would be expensive and public. Instead, only a 32-byte fingerprint is committed. When a bill is created, Splitsy computes a keccak256 hash over the bill's canonical fields and passes it to createBill(bytes32 metadataHash, address[] participants, uint256[] amounts). The contract emits BillCreated with that hash; it can never be edited afterward.

metadataHash = keccak256(
  abi.encode(
    merchant,           // string,   e.g. "ROYAL HANDI HUT"
    currency,           // string,   e.g. "USD"
    cents,              // uint256,  total in cents (e.g. 300 = $3.00)
    labels.join("|"),   // string,   participant labels in order
    receiptHash,        // string,   keccak256 of the receipt image ("" if none)
    dueDate             // uint256,  optional "pay by" Unix seconds — appended
  )                     //           ONLY when the creator set a deadline
)

The optional dueDate is a strictly additive commitment: a bill with no deadline encodes exactly as bills did before due dates existed, so every previously created bill still verifies byte-for-byte. When present, it anchors the deadline that payment reputation grades timeliness against — the creator cannot move it after the fact.

All-or-nothing bills

A bill with a due date can also be created all or nothing (escrowUntilFull). On a normal bill each payment is the creator's the moment it lands — they can claim it straight away. Tick the box and nothing is claimable until everypayer has settled. Use it when a partial amount is no good to you: six concert tickets, a group gift, a deposit. $160 does not buy six $40 tickets, and holding four people's money for a purchase that isn't happening helps nobody.

That is also why an all-or-nothing bill must have a due date; the registry rejects the pair at creation otherwise. Without a deadline there is no moment at which a short bill counts as failed, so in a contract with no owner, no pause and no sweep the money could never be released to anyone. With one, every route out is self-service: pay it off and the creator claims, or miss it and the payers withdraw.

A refund puts that payer's share back on the board rather than killing the bill, so a late payer — or an autopay agent calling payDebtFor — can still complete it afterwards, and the creator can still be paid in full. collectDebtalso still unlocks at the deadline on these bills, because pulling from debtors who granted a mandate is exactly what can carry a short bill over the line. Anything pulled that doesn't is refundable to the debtor it came from, so the mandate cannot be used to extract money from a failed bill.

The human-readable values behind that hash — the preimage— are published off-chain to Supabase so a payer's browser can recompute the hash and compare. The preimage is only a convenience transport: it is never trusted. The server that stores it first reads the real metadataHashback from Arc and refuses to save any preimage that doesn't hash to it, so a stored record is always genuine.

The receipt image is committed too

To make check 2 possible, the receipt itself is bound to the bill. In the creator's browser the photo is downscaled and re-encoded to a compact JPEG (~80 KB), then hashed with keccak256. That receiptHash is one of the fields inside metadataHash above, so the exact image is anchored on-chain. The image bytes themselves are uploaded to a public Supabase Storage bucket keyed by registry/billId. The publish route re-hashes the uploaded bytes and rejects anything that doesn't match the committed receiptHash, so the stored image is provably the committed one. Bills typed in by hand have no image and commit receiptHash = "".

What the payer's browser does

Verification runs entirely in the payer's browser and trusts only the chain — Supabase is just a delivery pipe. The two checks map directly to the two badge lines:

1

Recompute the fingerprint (authenticity)

Fetch the preimage, recompute keccak256 over its fields, and compare to the metadataHash read from Arc. A single altered character — merchant, a cent, a label, or the receipt hash — makes the fingerprints differ. Match ⇒ "Genuine bill on Arc." Mismatch ⇒ "Details don't match Arc — don't pay."

2

Re-hash the committed receipt (provenance)

Download the receipt image and re-hash its bytes. If the hash doesn't equal the committed receiptHash, the image is not the committed one and is neither shown nor trusted. If it matches, the payer is looking at the exact photo anchored on-chain.

3

Re-read the receipt and compare (honesty)

The browser independently OCRs that verified image, converting a non-USD total to USD with the same FX endpoint the creator used. It compares the receipt's own total to the committed total. Because the payer extracts the number themselves from a hash-locked image, a creator who committed a different figure is caught — no trust in the creator required.

What each badge state means

BadgeCheck 1 — genuine?Check 2 — total vs receiptWhat it means for you
Verified on ArcMatchReceipt total matchesSafe to pay: authentic bill and the amount matches the receipt.
Warning — the total was changedMatchReceipt reads a different amountReal bill, but the charged total disagrees with the receipt. Ask the creator first.
Genuine, no receiptMatchNo receipt to checkCreator typed the total by hand; there is no bill image to cross-check against.
Genuine, couldn't re-readMatchOCR/FX unavailableAuthentic bill; open the receipt and compare the total by eye.
This bill doesn't match ArcMismatchNot evaluatedWhat you're shown is not what was committed. Do not pay.

Payment Reputation

Every payer who settles an on-chain bill in full earns verifiable payment reputation using the ERC-8004registries Arc pre-deploys on testnet — no Splitsy contract is involved. The payer's wallet receives an identity NFT on the IdentityRegistry, and each completed payment is recorded as a scored feedback entry on the ReputationRegistry. When someone later tags that payer into a new bill, the creation form shows a badge: "Paid N bills in full on Arc · 97/100 timeliness".

RegistryArc Testnet addressRole
IdentityRegistry0x8004A818BFB912233c491871b3d84c89A494BD9eMints one ERC-721 identity NFT per payer wallet via register(metadataURI); the tokenId is the payer's agent id.
ReputationRegistry0x8004B663056A597Dffe9eCcC1965A193B7388713Stores each payment's score via giveFeedback(agentId, score, …, feedbackHash).

How a score is earned

1

A payment settles on-chain

The anchor is always a BillSplitRegistry.DebtPaid event (or a recurringsettleTabcollection). Only payments that complete the payer's full share (paidTotal ≥ owedTotal) are scored.

2

The payer gets an identity NFT (first payment only)

Registration is lazy: on the wallet's first scored payment, register()mints its identity NFT. A Circle-wallet payer's own wallet signs (it just paid, so it holds gas); for browser-wallet payers a dedicated registrar wallet mints on their behalf, then transfers the NFT to the payer — every payer ends up owning their own identity.

3

Timeliness is graded against the committed due date

The score compares the payDebt block timestamp (never a server clock) to thedueDatethe creator committed into the bill's metadata hash — a deadline that cannot be moved after creation.

4

A validator wallet records the feedback

A dedicated Splitsy validator wallet calls giveFeedback with the score, a timing tag, and a feedbackHash binding the entry to the exact payment transaction it scores.

The scoring curve

SituationTagScore
Bill had no due datepaid_in_full100
Paid by the due date + 2-day grace windowpaid_on_time100
Paid after the grace windowpaid_late100 − 5 per whole day late, floored at 50

Paying is always positive — even a very late payment is evidence of good faith, so the floor is a passing 50, and a payment that is never made simply records nothing. The badge's aggregate is an amount-weighted average: each payment is weighted by the payer's USDC share, so a large bill paid late drags the average more than a small one. Weighting happens only at aggregation; every on-chain score stays simple and independently verifiable. Recurring tabs score too: each settled cycle a member is collected from earns one independent score, graded against that cycle's boundary.

Three wallets, by design

ERC-8004 forbids an agent's owner from scoring its own agent, so Splitsy separates roles across three distinct wallets:

WalletWho it isWhat it does
PayerThe wallet that paid the billOwns (or is bound to) the identity NFT being scored. Circle-wallet payers sign their own registration.
RegistrarDedicated Splitsy Circle walletMints identity NFTs for browser-wallet payers, who never hand Splitsy a wallet to sign with, then transfers each NFT to its payer. It holds those NFTs at mint time — which is exactly why it must not also score them.
ValidatorA second dedicated Splitsy Circle walletRecords every giveFeedback. Distinct from the registrar and from all payer wallets, so the no-self-scoring rule always holds.

Verify a score yourself

Every feedback entry commits feedbackHash = keccak256("splitsy:bill:<billId>:<payTxHash>") (recurring cycles use splitsy:tab:<tabId>:cycle:<n>:<settleTxHash>), and its fileuri field carries the same payment hash as tx:<payTxHash>. That makes each score independently re-checkable against the payment it claims to describe, with nothing but a block explorer:

1

Find the feedback entry

On Arcscan, open the ReputationRegistry address above and locate the giveFeedback transaction (the badge's data mirrors feedback_txper entry). Read the decoded inputs: agent id, score, timing tag, bill tag, and feedbackHash.

2

Recompute the hash

Compute keccak256 of the UTF-8 string splitsy:bill:<billId>:<payTxHash> using the bill id from the tag and the payment hash from fileuri. It must equal the committed feedbackHash — one changed character breaks it.

3

Check the payment is real and complete

Open the payment transaction on Arcscan and confirm it emitted DebtPaid from the BillSplitRegistry with the same bill id, with the scored wallet as payer, and with paidTotal ≥ owedTotal.

4

Check the deadline it was graded against

Fetch the bill's published preimage (see Bill Verification) and recompute the metadata hash — the committed dueDateinside it is the deadline the timing score used, and the payment's block timestamp is the "paid at" moment. Apply the curve above and you reproduce the exact score.

5

Check the identity binding

On the IdentityRegistry, confirm the agent id from step 1 is the token minted in the payer's registration transaction — the mint's Transfer log carries the tokenId.

What the badge shows — and what it never does

  • The badge appears while tagging payers into a new bill, looked up by handle, email, or address.
  • It shows the count of bills paid in full, the amount-weighted timeliness average, and how many were late.
  • Looking up a handle never reveals the wallet address behind it — the API returns only the aggregate.
  • Looking up a handle never creates a wallet; wallets are only provisioned when a bill is actually created.
  • "No payment history yet" covers both "person unknown" and "wallet known, no payments" — deliberately indistinguishable.

Optionally, each identity NFT's metadataURI points to an agent profile pinned to IPFS (name, agent type, wallet). Without IPFS configured, registration falls back to an inline data: URI — the reputation mechanics are identical either way.

Recurring Tabs

Recurring tabs are fixed-share payment schedules. A creator chooses a recipient, interval, maximum number of settlement cycles, member wallets, and each member's fixed USDC share. Members approve the tab contract as a constrained USDC spender. Funds remain in member wallets until settlement runs.

Factory deployment

RecurringTabFactory deploys one RecurringTab contract per tab. Each tab has immutable recipient, interval, max cycle count, member list, and fixed shares.

Scheduled settlement

Splitsy checks factory-created tabs on a schedule and calls settleTab() for tabs that have collectible balances.

Shortfall handling

If a member has insufficient allowance or balance, the contract emits shortfall events and collects from members who are ready. Late underpaid amounts can be collected later after approval or funding.

Claimable balance

Settlement increases tab-level claimable. The recipient can call claim() to withdraw collected funds.

The debtor view shows approved amount, wallet balance, paid total, total debt, cycles due, and progress. A paid tab uses a paid-bill stamp. The splitter view shows every member's share, due amount, remaining total, wallet balance, allowance, and collected total.

Circle and Arc

Splitsy uses Circle and Arc technology for USDC movement and settlement. Arc Testnet is the destination network for the app's contracts. Circle Gateway enables cross-chain USDC payments from any supported source chain directly to Arc Testnet in one flow.

Circle Gateway

Pay from any supported testnet (Avalanche Fuji, Base Sepolia, Ethereum Sepolia) and settle on Arc. Two-step flow: sign an EIP-712 burn intent on the source chain (gas-free), then execute the mint transaction on Arc. No bridge UI, no waiting — native USDC moves chain-to-chain in seconds.

CCTP

Circle's Cross-Chain Transfer Protocol burns USDC on the source chain and mints it on the destination chain. Gateway wraps CCTP with a permissionless API that returns an attestation, so the payer's wallet can execute the mint directly without holding for manual attestation fetching.

Browser wallets

Splitsy discovers wallets with EIP-6963 announcements, requests accounts through EIP-1193, prefers MetaMask when available, and uses the wallet provider for signing. Gateway payment requires the wallet to switch chains twice: once to sign the burn intent on the source chain, once to execute the mint on Arc.

Arc properties

Arc is EVM-compatible, uses USDC as its gas token in the Arc environment, and supports CCTP-based USDC bridging. Gateway's GatewayMinter contract on Arc (0x0022222A...) handles the final mint step after attestation.

Autopay Agents

When someone raises a bill against you, you can have it settled without opening the app. The thing that settles it is your own agent: a wallet on Arc that belongs to your Splitsy account, holds its own USDC balance, carries its own ERC-8004 identity NFT, and spends strictly under ceilings you set. It draws only on what you have sent it — Splitsy takes no allowance on your own wallet to make this work.

One agent per account

Its wallet is keyed to your account, not to a wallet, so the same agent and the same balance cover both your Splitsy wallet and any browser wallet you have linked. You fund it once.

Its balance is the hard ceiling

Funding is a plain USDC transfer to the agent — custody, not permission. An agent holding 5 USDC can never spend 6, whatever any rule or bug says, because it has nothing else to draw on.

Rules are checked before it spends

Per-bill ceiling, per-day ceiling, an allowed-creator list, a creator score floor, a verified-hash requirement, and a bill-contents review. Every one is a ceiling evaluated before payment, never a target.

Every settlement is an audited job

The payment is wrapped in an ERC-8183 job: your agent posts and escrows a fee, a Splitsy agent does the work, and a third agent is paid to check the debt really settled before that fee is released.

Funding your agent

The Fundbutton sits next to the agent's balance on the settlement-agents panel. Whichever route you use, it is an ordinary inbound USDC transfer on Arc Testnet — there is no special deposit contract, and you can verify the balance yourself on Arcscan.

RouteWhat happensWhat it needs
From a connected browser walletYour wallet signs a USDC transfer to the agent's address. Splitsy waits for the receipt and checks it succeeded.A wallet connected on Arc Testnet with USDC.
From your Splitsy walletThe same transfer, sent server-side from your Circle wallet.Your wallet PIN unlocked — the same five-minute unlock a normal send uses.
From anywhere elseSend USDC to the agent's address from any wallet or faucet. Nothing in the app needs to know.Just the address, shown on the card and linked to the explorer.

Three things come out of that one balance on every settlement: your share of the bill, the job fee(0.01 USDC by default, escrowed and released to the agent that did the work), and the agent's own gas. Before it starts, the agent checks it holds the fee plus a 0.20 USDC gas headroom plus the share itself; short of that it skips with agent_unfunded and opens no job, so an underfunded agent costs you nothing. Top it up and the next bill settles.

Three agents on every job

The settlement itself is not a single hidden server call. It is an ERC-8183 job on the already-deployed AgenticCommerce contract on Arc Testnet, with three distinct wallets in three roles, so no agent ever grades its own work:

RoleWhoWhat it does
ClientYour agentPosts the job and escrows the fee out of your balance.
ProviderThe Splitsy SettlerPrices the work, buys the bill review, settles the debt, and submits proof of what it did.
EvaluatorThe Splitsy AuditorReads the registry on chain and releases the escrow only if the debt really is settled.
0

Decide — and buy a second opinion

Your rules run first against the bill. If they say pay and the contents check is on, the Settler buys a review of the bill from the Auditor over x402. Any refusal stops here: no job is created and no transaction is sent, so a skip costs nothing.

1

createJob — your agent

Your agent opens the job naming the Settler as provider, the Auditor as evaluator, a description identifying the bill and debtor, and an expiry one hour out.

2

setBudget — the Settler

The provider prices its own work at the settlement fee. The client does not set the provider's price.

3

fund — your agent

The fee moves from your agent's balance into escrow. The bill money is never in the escrow — only the fee.

4

settle — the debt is paid

BillSplitRegistry.payDebtFor(billId, debtor, amount) is called by your agent, paying your share out of its own balance. This is the only step that moves bill money.

5

submit — the Settler

The Settler submits keccak256(settlementTxHash) as the deliverable, so anyone holding the settlement transaction can recompute it and check the job against it.

6

complete — the Auditor

The Auditor calls getParticipant on the registry itself and completes the job only when paid ≥ owed. Otherwise it does not complete, the job expires, and the Settler is not paid.

The bill review is bought, not asked for

"Check the bill's contents before paying" is not a free internal function call. The Auditor sells that verdict at $0.002 per review and the Settler buys it over x402 — the same HTTP 402 Payment Required protocol Scout uses — paying out of the fee income it earns from completed jobs. The review weighs the merchant, total and your share against each other; it is given headline figures only and never the receipt image, so it cannot tell who ordered what.

Every failure direction is a refusal: a 402, a timeout, an unparseable verdict, a missing key, or a failed x402 settlement. A Settler that cannot buy a review settles nothing.

Splitsy's paid endpoints

EndpointPriceSellerBuyer
/api/ocr$0.005 USDCSplitsyScout, per receipt scan
/api/fx$0.001 USDCSplitsyScout, only for non-USD receipts
/api/agents/review$0.002 USDCThe Splitsy AuditorThe Splitsy Settler, before every settlement

All three are open to anyone who pays — that is what makes them a market rather than an internal call. Each is settled by Circle's batch facilitator against an offchain EIP-3009 authorization, so the buying agent spends no gasto pay, and both sides of every payment are recorded in Splitsy's x402 ledger.

Reading the decision log

Every bill your agent looked at leaves a row, including the ones it refused — the refusals are the point, because they are what shows a spending permission is still constrained. Each row carries the bill, the amount, the decision, and a reason:

ReasonWhat happened
agent_unfundedThe balance could not cover the share plus the fee plus gas headroom. No job was created. Top it up.
over_bill_cap / over_daily_capAbove your per-bill or per-day ceiling.
untrusted_creator / low_creator_scoreThe creator is not on your allowed list, or their payment reputation is below your floor.
hash_mismatch / unverifiableThe bill's details do not match what was committed on chain, or nothing was published to check against.
review_unavailableThe paid review refused or could not be read. Fail-closed: nothing was paid.
job_failed / tx_failedA job transaction reverted, or the settlement transaction itself failed.
nothing_owed / disabledThe share was already settled, or autopay is switched off.

A settled row expands into its job trail: every transaction of the ceremony with its block number and hash, the job's live status read from the contract, and the x402 payments that gated it, each linking to Circle's own receipt. The status stored on the row is a display mirror; the contract is the source of truth.

Job statusMeans
completedThe full ceremony ran; the Auditor verified the debt and released the escrow.
settled_incompleteYour debt is paid. Only the submit or complete step broke afterwards.
settlement_unconfirmedThe settlement was broadcast but not confirmed in time; it may still mine.
failedThe ceremony broke before the payment step. No money moved and none can.

The last three are deliberately logged as a payment for the full amount whenever the money might have moved, and they count against your daily ceiling. Costing you headroom you were entitled to is recoverable; handing back a cap you had already spent is not.

What it costs to run

Six transactions per settled share

Not per bill — per share. A four-person bill where everyone autopays is four independent jobs. A skip costs zero, because the decision happens before the job is opened.

0.01 USDC fee, at risk of nothing else

The escrow only ever holds the fee. If a settlement fails the job simply expires an hour later, and at worst that fee is stranded — the bill money is never inside the escrow in the first place.

Two USDC approvals sit outside those six. They are lazy — sent only when the current allowance is short, and for 100× the amount being spent — so they amortise across roughly a hundred settlements instead of landing on each one.

Scout Agent

When you upload a receipt, Splitsy does not scan it directly. The upload is handed to Scout — an autonomous agent with its own wallet, its own ERC-8004 on-chain identity, and a daily spending budget. Scout decides whether your photo is worth scanning, then pays Splitsy's own scanning API in USDC, per call, over Arc. If the first read looks shaky it buys a second opinion out of its own budget.

This is a real machine-to-machine economy, not a metaphor: every scan is an HTTP request that gets answered with 402 Payment Required, a USDC payment authorization, and only then the parsed bill. You never pay for it and never see a prompt — the agent's spending is its own.

It judges before it spends

Scout checks the photo first. Under 8 KB, or under 200 px on either edge, and it refuses to pay at all — you get asked for a clearer picture instead. Nothing is spent on an unreadable image.

It pays per call over HTTP

Splitsy's /api/ocr and /api/fx are paywalled with the x402 protocol. Scout signs an offchain EIP-3009 authorization instead of sending a transaction — so it pays for the API and burns no gas doing it.

It buys a second opinion

Each parse carries a confidence score. Below 0.80, and with budget left, Scout pays a second time for a stricter re-read, then keeps whichever parse scored higher.

It has a hard budget

A daily cap (default $1.00USDC) is the agent's risk control. When the cap is reached Scout stops paying and returns its best-effort read, flagged as low confidence — it can never overspend.

What a single scan actually does

1

Assess the image — no spend yet

Scout reads the file size and pixel dimensions. Too small or too low-resolution and it declines with a reason, having paid nothing.

2

Request the scanner, get a 402

Scout calls /api/ocr. The endpoint answers 402 Payment Required with a PAYMENT-REQUIRED header quoting the terms: scheme exact, network eip155:5042002 (Arc Testnet), the USDC asset, and the amount in atomic units (5000 = $0.005).

3

Sign an authorization, not a transaction

Scout signs an offchain EIP-3009 authorization from its wallet and retries the same request with a payment-signature header. No transaction is broadcast at this point, so the agent spends no gas.

4

Circle verifies and settles

Splitsy's server hands the authorization to Circle's batch facilitator, which verifies it and settles the USDC. Only then does the endpoint run the scan and return the parsed bill, with a PAYMENT-RESPONSE header carrying the settlement reference.

5

Check confidence, maybe pay again

If confidence is under 0.80 and the daily cap allows it, Scout repeats the paid call with a stricter re-read instruction and keeps the better of the two parses.

6

Convert the currency if needed

If the receipt is not in USD, Scout pays /api/fx ($0.001) for a rate to quote the total in USD — the same figure your split is calculated from.

What Scout charges itself

Paid endpointPrice per callWhen Scout calls it
/api/ocr$0.005 USDCOnce per scan; a second time if the first parse scores under 0.80 confidence.
/api/fx$0.001 USDCOnly when the receipt's currency is not USD.

So a clean USD receipt costs the agent $0.005; a blurry euro receipt costs $0.011. Both sides of every payment are recorded — what Splitsy earned as the seller and what Scout spent as the buyer — and the dashboard's Scout's x402 ledgerpanel shows the running totals, the budget left for the day, and the last payments with a link to Circle's receipt for each. Scout is not the only agent that pays this way: see Autopay Agents for the review the Splitsy Settler buys before every settlement.

Scout's wallet and on-chain identity

Scout does not use a Circle wallet like yours. It holds a dedicated server-side account on Arc whose onlyjob is signing x402 payment authorizations — deliberately separate from every user wallet, and funded with a small amount of test USDC deposited into Circle's Gateway so its payments can be batched.

It is also registered as an agent on the same ERC-8004 IdentityRegistry that gives payers their reputation NFTs (0x8004A818BFB912233c491871b3d84c89A494BD9e), via register(metadataURI). That means the agent that scanned your receipt has a publicly checkable identity: the scan result shows "scanned by agent 0x… — see its onchain identity" and links straight to Arcscan.

Net-Settlement Treasury

Once you have joined a few bills, what you owe and what you are owed is scattered across all of them. The dashboard's Treasury tab collapses that into one net figure per person — and a single Settle net button that discharges every open position at once.

The open ledger

Every share you owe on bills others created, and every unpaid share owed to you on bills you created — read live from the registry on Arc, not from a cached balance.

One net position

Both directions with the same person are folded together. If Alex owes you $8 on one bill and you owe Alex $12 on another, you see a single −$4.00, sorted so your largest exposure is first.

One settlement

Settle net pays every debt and collects every claimable bill in one action. On a Splitsy wallet the whole thing is one atomic transaction.

Claimable, separately

Money already paid to you but not yet withdrawn is shown as its own figure — it is yours to collect and is included in the same batch.

What Settle net costs to run

Settling bill by bill means an approval plus a payment for every debt, and a claim for every bill you are collecting on — 2 × debts + claims transactions in total. What replaces that depends on which wallet signs:

Signing walletTransactions to settle everythingFailure behaviour
Splitsy wallet (social sign-in)1 — every approval, payment and claim in one atomic batchAll-or-nothing: if any leg would fail, the entire batch reverts and nothing settles.
Connected browser wallet1 approval + 1 per debt + 1 per claimSequential: the progress modal shows exactly which step is running, and a later step failing leaves earlier ones settled.

The asymmetry is not arbitrary. Splitsy's social wallets are Circle smart contract accounts, which can execute a batch of calls as one atomic transaction; a connected browser wallet is a plain externally-owned account, which cannot, so it still signs each leg. Either way a single USDC approval covers every payment instead of one approval per bill.

Settling from your Splitsy wallet

1

Unlock the wallet

Because this moves money, Settle net requires your wallet PIN to be unlocked — the same five-minute unlock used for a normal send. Locked, the button tells you to unlock first.

2

Amounts are re-read from chain

Every outstanding amount is read fresh from the registry at the moment you press the button. Nothing the browser sent is trusted, so a stale dashboard can never cause a wrong amount to be signed.

3

One batch is assembled and sent

One USDC approval for the summed total, one payment per debt, one claim per collectible bill — packed into a single atomic transaction against your own wallet account.

4

Reputation is scored as usual

Each debt settled in full earns payment reputation exactly as an individual payment would (see Payment Reputation). Batching changes the transaction count, not the consent rules or the scoring.

Reading the Treasury tab

  • Owed to me — the sum of every unpaid share on bills you created.
  • I owe — the sum of every unpaid share you hold on other people's bills.
  • Net position — the difference. Positive means you are owed on balance; negative means you owe.
  • Claimable now — money already paid into your bills that you have not yet withdrawn.
  • Per person, both directions are shown alongside the net, labelled with their handle where Splitsy knows it and a shortened address where it does not.
  • The tab shares the dashboard's wallet scope selector. With both a Splitsy wallet and a browser wallet connected you must pick which one settles, because each signs differently.
  • On sample data (?demo=1) the figures render but settling is disabled.

Architecture

Experience layer

The Splitsy web app handles receipt upload, bill review, split editing, wallet connection, debt payment, claim flows, recurring tab creation, approval management, tab selection, and event display.

Service layer

Receipt extraction, currency conversion, and recurring settlement automation are handled outside the payment interface so users only see the actions they need.

Contracts

BillSplitRegistry stores one-time debts. RecurringTabFactory creates recurring tab contracts. RecurringTab handles fixed-share scheduled collection and claimable balances.

Integration layer

Wallet, contract, and Circle bridge integrations are separated from the interface so payment flows remain consistent across one-time bills and recurring tabs.

Application interfaceA responsive web experience for receipt upload, bill review, split creation, wallet connection, and recurring payment management.
Typed transaction layerStrongly typed contract reads and writes for USDC payments, approvals, event history, and Arc Testnet wallet interactions.
Solidity contractsBill registry and recurring tab contracts define the accounting rules that keep payments verifiable onchain.
Circle GatewayPermissionless cross-chain USDC payments from Avalanche, Base, or Ethereum into Arc Testnet using EIP-712 burn intents and Gateway attestation.
CCTPNative USDC burn-and-mint movement between supported source chains and Arc, underpinning the Gateway flow.
Settlement automationProtected automation checks recurring tabs on a schedule so payers do not need to press a settle button each cycle.
Agent economyPer-account autopay agents settle debtor shares as ERC-8183 jobs, with an independent evaluator releasing the escrowed fee and paid bill review bought over x402.

Contracts

Contracts are intentionally narrow. They store the minimum accounting state needed for payment enforcement and emit events for app indexing, explorer review, and user-facing history.

ContractPurposeImportant events
BillSplitRegistryCreates bills, records participant debts, accepts partial or full payments, and lets splitters claim paid funds.BillCreated, DebtPaid, FundsClaimed
RecurringTabFactoryDeploys isolated recurring tab contracts and forwards scheduled settlement calls by tab id.TabCreated
RecurringTabCalculates accrued member obligations, transfers available USDC, records shortfalls, and exposes claimable funds.MemberSettled, SettlementShortfall, TabSettled, FundsClaimed

The payment contracts build on a small set of shared, audited security primitives rather than external dependencies. Each is intentionally minimal and carries no owner, upgrade, or privileged path.

ModuleTypeRole
ReentrancyGuardAbstract baseProvides the nonReentrant modifier. Every fund-moving entrypoint (payDebt, claim, settleTab) inherits it, so a function cannot be re-entered while it executes.
SafeERC20LibraryWraps transfer and transferFrom so a token that returns no data or false can never be mistaken for a successful transfer; any non-success reverts with SafeERC20FailedOperation.
IERC20InterfaceMinimal ERC-20 surface (allowance, balanceOf, transfer, transferFrom) the contracts use to read approvals and balances and to move USDC.

Operations

Recurring settlement is designed to be automatic after user approval. Payers maintain enough USDC and allowance for the tab, while Splitsy periodically checks whether a cycle is due and collectible.

If a payer has insufficient balance or allowance, the contract records a shortfall and Splitsy can collect the unpaid portion later after the payer funds or re-approves their wallet. Recipients can claim collected funds when a claimable balance is available.

Autopay agents are the debtor-side equivalent and are funded by the user, not by the operator: an agent that runs out of USDC skips with agent_unfunded and creates nothing on chain, so restoring it is a top-up rather than an operator action. The Splitsy Settler and Auditor pay for their own transactions out of their own balances, and an unset settlement configuration reads as autopay off— never as "settle without the job".

Security

  • Users explicitly approve USDC spend before contracts can pull funds.
  • Recurring approval is constrained to the tab contract address and can be revoked by setting allowance to zero.
  • Recurring settlement is protected by operational controls and is not exposed as a public user action.
  • Every fund-moving entrypoint follows checks-effects-interactions and is guarded by the shared ReentrancyGuard (nonReentrant) module.
  • All USDC movement routes through the SafeERC20 library, so a token that returns no data or false can never be treated as a successful transfer.
  • Contracts hold no privileged owner and expose no upgrade, pause, sweep, or selfdestruct path; funds can only ever leave to a bill's splitter or a tab's immutable recipient.
  • Sensitive operational credentials must never be exposed in browser code, screenshots, public docs, or client logs.
  • Contracts use custom errors and explicit checks for invalid amounts, unknown bills, unauthorized claims, and duplicate recurring members.
  • Receipt OCR data should be reviewed by the splitter before submission. The scanner is a convenience layer, not an accounting authority.
  • Bridge flows depend on the connected wallet signing each step and on Circle attestation for CCTP minting.
  • Payment reputation is consent-based and positive-only: a score can only be created by a payment the wallet itself made, and every entry is re-verifiable against the on-chain payment it commits to (see Payment Reputation).
  • An autopay agent spends only the USDC you transferred to it — Splitsy holds no allowance on your own wallet for it — so its balance is a hard ceiling no rule, bug, or compromised server can exceed.
  • Each settlement job uses three distinct wallets for client, provider, and evaluator, so the agent that is paid for a job is never the agent that decides it was done. The evaluator re-reads the registry on chain rather than trusting the provider's claim.

Configuration

Splitsy should be connected to the intended Arc Testnet contracts before users create bills or recurring tabs. Contract addresses, USDC token settings, bridge support, receipt scanning, and settlement automation are managed by the operator during deployment.

For users

Use a compatible browser wallet, switch to the supported Arc Testnet network, keep enough USDC for payments, and review every wallet prompt before signing.

For operators

Keep sensitive operational configuration outside public documentation. Publish only user-safe details such as supported network, supported asset, verified contract addresses, and contract source links.