Pair-Programming a Wallet on FRAGMENT

Sep 19, 2025
Steven H. Klaiber-Noble
Developer

How an LLM used FRAGMENT to delete code I thought I had to write

Me: "I want to build a wallet for my app. Can you help?"

LLM: "Great, let's build a wallet - But first what is a wallet?"

Me: "It's the back-end that keeps score every time money moves inside an app: your Starbucks balance, an in-game currency, Venmo."

LLM: "Sounds easy. But you know it's never just a balance, right?"

Me: "And that's why I'm looking at FRAGMENT: a service that gives you a production-grade wallet backend with all the hard parts (like concurrency, idempotency, and reconciliation) built in."

Building a wallet is harder than it looks — especially if you want it to be safe and reliable. FRAGMENT gives you a fully configurable database for money, so you can model and power virtually any type of fintech app, from wallets to marketplaces to neobanks, without having to build all the tricky logic yourself. You can use its wallet template to get started fast, or design your own funds flows from scratch. Under the hood, it uses a double-entry accounting ledger, but you don't need to know accounting to use it. While FRAGMENT offers a wallet template for common use cases, you can configure it to back virtually any fintech app: wallets, marketplaces, neobanks, rewards, and more.

A double-entry ledger is a system where every transaction is recorded in two places—like "money in" and "money out"—so your balances always add up. It's the same system banks use to prevent errors and fraud.

What actually hides behind "wallet"

A production wallet has to do seven distinct chores:

  1. Keep a running balance for every user
  2. Accept deposits / top-ups
  3. Move money between users (transfers & payouts)
  4. Stop two simultaneous actions from corrupting a balance (race conditions)
  5. Guarantee idempotency so network retries don't double-post
  6. Reconcile internal ledger lines with the payment processor
  7. Serve yesterday's balance instantly, while new transfers fly in

Why not just use a database?

You are still using a database. FRAGMENT is the database for money—it's just designed to handle all those financial headaches like double-counting, race conditions, and missing transactions. Think of it as your regular database's financially savvy cousin who actually remembers to balance the checkbook, so you don't have to write (or debug) all that tricky logic yourself.

LLM: "Most of that is built in to FRAGMENT.dev—a hosted double-entry ledger. Want to see how much code we can delete?"

The starting point — all chores, no Fragment yet

Hand-rolled vs Fragment wallet comparison
+---------------------+      +----------------------+
|  Hand-Rolled Wallet |      |   Fragment Wallet    |
+---------------------+      +----------------------+
| - Manual SQL        |      | - Declarative Schema |
| - Race Guards       |      | - API Call           |
| - Idempotency Table |      | - Built-in Safety    |
| - Reconciliation    |      | - 6 lines of code    |
+---------------------+      +----------------------+
Figure: Hand-rolled wallet logic vs. FRAGMENT.dev approach.
Bare-bones wallet with TODOs
# wallet_before_fragment.py
class WalletService:
    """
    Bare-bones wallet. Every TODO is a job the LLM will soon erase with <InlineLogo />.
    """
    def __init__(self, db):
        self.db = db  # balances & ledger tables

    #1 Add money (deposit)
    def deposit(self, user_id: str, cents: int, tx_id: str):
        """Add money, idempotently."""
        # TODO idempotency guard
        # TODO write ledger line
        # TODO update balance with concurrency safety
        ...

    #2 Move money (transfer)
    def transfer(self, from_id: str, to_id: str, cents: int, tx_id: str):
        """Move money between two users."""
        # TODO overdraft guard
        # TODO two ledger lines (debit + credit)
        ...

    #3 Reconcile with payment processor
    def reconcile_charge(self, charge_id: str):
        """Match Stripe charge to ledger line and mark reconciled."""
        # TODO locate internal line
        # TODO mark reconciled
        ...
 

Keep the TODOs in mind—each upcoming section shows the LLM flagging one chore and FRAGMENT deleting the helper code. When we reach the end this class is six lines long.

How Fragment Works (for Developers)

You interact with FRAGMENT through its API—just like you'd call Stripe or Twilio. Instead of writing all the logic for deposits, transfers, and balance checks yourself, you describe what should happen in a simple schema (a bit like a config file). FRAGMENT then enforces all the rules for you:

  • Atomicity: No partial updates—transactions either fully succeed or fail.
  • Idempotency: Safe to retry API calls; no double-posting.
  • Balance Safety: Prevents negative balances and race conditions.

FRAGMENT is not just for wallets: you can model any kind of funds flow, from peer-to-peer payments to complex marketplace settlements. You don't need to know accounting—just describe your business logic, and FRAGMENT handles the rest.

Figure: How Fragment.dev turns your schema into a safe, production wallet.

1 Minimal Deposit → Balanced Entries

Me: "Okay, I need to add money to a user's wallet, but I want to make sure it's idempotent, updates the balance, and keeps the ledger straight. How do I do all that without writing a ton of code?"

LLM: "Your deposit needs three things: idempotency, a debit/credit pair, and a balance update. FRAGMENT can do all three if you define a balanced entry schema. Ready to delete some code?"

Me: "Wait, so instead of writing all that logic in Python, I just describe what a deposit should do in a schema and FRAGMENT enforces it?"

LLM: "Exactly. The schema is your business logic. FRAGMENT takes care of the rest—atomicity, idempotency, and making sure your books always balance. (See the Appendix for how to initialize the SDK client.)"

Here's what that business logic looks like as a Fragment schema:

Wallet schema
{
  "type": "DepositEntry",
  "description": "Adds cash to a user's wallet",
  "lines": [
    {
      "key": "credit_wallet",
      "account": { "path": "liability/wallets/wallet:{{user_id}}", "consistencyConfig": { "ownBalanceUpdates": "strong" } },
      "amount": "{{cents}}"
    },
    {
      "key": "debit_cash",
      "account": { "path": "asset/cash" },
      "amount": "{{cents}}"
    }
  ]
}
This schema says: "When a deposit happens, add money to the user's wallet and subtract it from the cash account. Both sides must always add up—so your balances are always correct."

Code after FRAGMENT

Deposit code

# Note: All SDK calls are async and must be awaited. See the Appendix for setup details.
async def deposit(self, user_id: str, cents: int, tx_id: str):
    await fragment.post_entries(
        "DepositEntry",
        idemp_key=tx_id,
        user_id=user_id,
        cents=cents,
    )

Deleted: manual balance math, ledger insert, race-safe transaction wrapper Fragment guarantees: lines net to 0, atomic commit, idempotent replay detection

2 Race-Proof Transfer → Conditional Entries

Me: "Now I want to let users send money to each other, but I'm worried about two transfers happening at once and overdrawing the sender. How do I keep it safe?"

LLM: "Two withdrawals at once could overdraw the sender. FRAGMENT has conditions—think SQL Check that runs inside the transaction."

Schema

Transfer entry schema
{
  "type": "TransferEntry",
  "description": "Move money between users",
  "conditions": [
    {
      "account": { "path": "liability/wallets/wallet:{{from_id}}", "consistencyConfig": { "ownBalanceUpdates": "strong" } },
      "precondition": {
        "ownBalance": { "gte": "{{cents}}" }
      }
    }
  ],
  "lines": [
    {
      "key": "debit_sender",
      "account": { "path": "liability/wallets/wallet:{{from_id}}", "consistencyConfig": { "ownBalanceUpdates": "strong" } },
      "amount": "-{{cents}}"
    },
    {
      "key": "credit_recipient",
      "account": { "path": "liability/wallets/wallet:{{to_id}}", "consistencyConfig": { "ownBalanceUpdates": "strong" } },
      "amount": "{{cents}}"
    }
  ]
}
 
This schema says: "Only allow the transfer if the sender's wallet has enough funds. Then, subtract from the sender and add to the recipient—always keeping the books balanced."

Code Now

Transfer function
# Note: All SDK calls are async and must be awaited. See the Appendix for setup details.
async def transfer(self, from_id, to_id, cents, tx_id):
    await fragment.post_entries(
        "TransferEntry",
        idemp_key=tx_id,
        from_id=from_id,
        to_id=to_id,
        cents=cents,
    )
Deleted: overdraft guard, two-phase balance read/lock, dual ledger writes

3 Idempotency & Retries → Built-in Keys

Me: "What about retries? I don't want a network hiccup to double-post a transaction. Do I need to build a table to track seen transactions?"

LLM: "Remove your tx_seen() table—every post_entries() call already takes an idemp_key. On duplicate keys FRAGMENT returns the original result."

4 Reconciliation → Linked Accounts

Me: "How do I match up Stripe charges with my internal ledger lines? That reconciliation loop is always a pain."

LLM: "Stripe already sends the charge_id; store it as external_ref and let FRAGMENT link it."

Reconcile Code Shrinks To:

Reconcile function
# Note: All SDK calls are async and must be awaited. See the Appendix for setup details.
async def reconcile_charge(self, charge_id, user_id):
    await fragment.reconcile_tx(
        external_ref=charge_id,
        user_id=user_id  # Provide all required parameters for parameterized entry types
    )
Deleted: SQL lookup, status update, error handling for missing lines

5 The Final WalletService

Final WalletService class
class WalletService:
    def __init__(self, ledger):
        self.ledger = ledger

    async def deposit(self, user_id, cents, tx_id):
        await self.ledger.post_entries("DepositEntry",
                                 idemp_key=tx_id,
                                 user_id=user_id,
                                 cents=cents)

    async def transfer(self, from_id, to_id, cents, tx_id):
        await self.ledger.post_entries("TransferEntry",
                                 idemp_key=tx_id,
                                 from_id=from_id,
                                 to_id=to_id,
                                 cents=cents)

    async def reconcile_charge(self, charge_id, user_id):
        await self.ledger.reconcile_tx(
            external_ref=charge_id,
            user_id=user_id  # Provide all required parameters for parameterized entry types
        )
 
Net result: 6 active lines of business logic
Lines we deleted: 120+ (balance tables, SQL locks, retry tables, reconciliation loop)

6 Scoreboard

Figure: How much code FRAGMENT.dev lets you delete for each wallet chore.

Success

You just watched an LLM talk me out of writing 120 (97%) lines of fragile wallet code by leaning on FRAGMENT.dev's built-in primitives.

LLM: "Ready to try it yourself?"
You: Get your free FRAGMENT workspace →

Glossary- API: A way for your code to talk to another service.

  • Double-entry ledger: An accounting system that always keeps your books balanced by recording every transaction in two places.
  • Idempotency: Making sure repeating the same API call doesn't double-post a transaction.
  • Atomicity: Guaranteeing that a transaction either fully happens or doesn't happen at all.

Integrations & Reconciliation

Me: "How do I keep my ledger in sync with Stripe or my bank? I bet that's a whole ETL pipeline in Python."

LLM: "Nope just link your Stripe account in the dashboard, and FRAGMENT will pull in charges and payouts."

Schema update: Add an external account link (e.g., Stripe)

Python SDK call to reconcile (with codegen):

Reconcile with external system
await client.reconcile_tx(
    external_ref=charge_id
)

Me: "So I don't have to write custom scripts to match up every payout?"

LLM: "Nope. FRAGMENT automates reconciliation, so your books always match."

Use Cases Beyond Wallets

Me: "What else can I build with FRAGMENT? I guess if I want to add lending or a marketplace, that's a whole new service in Python?"

LLM: "Nope just describe the new flow in your schema, and use the generated SDK methods."

Example: Add a new entry type for a loan disbursement or a marketplace escrow in your schema

  • Use the generated method for that entry type

Python SDK call (with codegen):

Loan disbursement example
# If you use the codegen client, you get type-safe methods for each entry type.
# See the Appendix for setup details.
await client.loan_disbursement_entry(
    idemp_key=tx_id,
    borrower_id=borrower_id,
    amount=amount,
    currency="USD",
)

Me: "So it's a toolkit for any money-moving app, not just wallets."

LLM: "Exactly. If you can describe the flow, you can build it in FRAGMENT.dev—no extra backend code required. And your Python code stays clean and clear."

Full Transaction History, Instantly

Transaction history
+-------------------+-------------------+
|   Date/Time       |   Description     |
+-------------------+-------------------+
| 2024-06-01 10:00  | Deposit $10       |
| 2024-06-01 10:05  | Transfer to Alice |
| 2024-06-01 10:10  | Received from Bob |
+-------------------+-------------------+

[ ... more lines ... ]
Figure: Every transaction is tracked and queryable—no more missing data.

What Will You Build?

FRAGMENT isn't just for wallets. It's a toolkit for any app that moves money, tracks balances, or needs bulletproof financial logic. Ready to delete your first hundred lines of wallet code?

  1. Book a demo
  2. Copy the Wallet Schema from this post
  3. Run the Python SDK example (with codegen) Done! You've got a production-grade wallet.
  4. Explore the docs

Appendix: Python SDK Setup & Async Usage

To use the FRAGMENT Python SDK, you must initialize the client with your credentials and use async/await for all SDK calls. Here's how to get started:

Python SDK setup and usage
from fragment.sdk.client import Client
import asyncio

client = Client(
    client_id="<Client ID>",
    client_secret="<Client Secret>",
    api_url="<API URL>",
    auth_url="<OAuth URL>",
    auth_scope="<OAuth Scope>",
)

async def main():
    # Example: get workspace
    response = await client.get_workspace()
    print(response.workspace.name)

    # Example: post a deposit entry (generic client)
    await client.post_entries(
        "DepositEntry",
        idemp_key="tx123",
        user_id="user_1",
        cents=1000,
    )

    # Example: with codegen client (after running codegen)
    # await client.deposit_entry(idemp_key="tx123", user_id="user_1", cents=1000)

if __name__ == "__main__":
    asyncio.run(main())
 
  • All SDK methods are async and must be awaited.
  • Use asyncio.run() or an event loop to run your async functions.
  • If you use the codegen tool, you'll get type-safe methods for each entry type (e.g., deposit_entry, transfer_entry).
  • For more, see the Docs or our GitHub repo.
LLM: "If you use a condition (like a precondition on ownBalance) in a ledger entry, the referenced account must have consistencyConfig: { ownBalanceUpdates: 'strong' }. This ensures balance checks are always up-to-date and safe.
Note: When using a condition (like a precondition on ownBalance) in a ledger entry, the referenced account must have consistencyConfig: { ownBalanceUpdates: 'strong' } (not consistencyMode). This is required for safe, up-to-date balance checks.