Skip to main content
Build a banking platform where crypto wallets sit alongside traditional fiat accounts — same look, same feel. Dfns powers the crypto side invisibly; customers never know it exists.

Get the code

dfns/dfns-solutions — bank-custody

Overview

The system models a retail bank that offers crypto wallets to its customers:
  • Customers see their fiat accounts (EUR checking, savings) and crypto wallets in one dashboard
  • The bank holds a Dfns service account — customers never interact with Dfns directly
  • Transfers below a threshold execute immediately; larger transfers require bank employee approval
  • Family delegation lets parents share wallet access with kids, with per-person transfer limits
┌─────────────────────┐
│   Next.js Frontend  │     ┌─────────────────────┐     ┌─────────────┐
│  (React, Auth0)     │────▶│   Flask Backend      │────▶│   Dfns API  │
└─────────────────────┘     │   (Python REST API)  │     │             │
                            │  • Auth0 JWT verify  │     │  • Wallets  │
                            │  • SQLite            │     │  • Txns     │
                            │  • Dfns Python SDK   │     └─────────────┘
                            └─────────────────────┘
The backend is a pure REST API with JWT auth — any client (web, mobile, CLI) can consume it.
LayerTechnology
FrontendNext.js 15 + TypeScript
BackendFlask (Python)
DatabaseSQLite
AuthAuth0 (RBAC for employee role)
Crypto custodyDfns Python SDK (service account)
BlockchainEthereum Sepolia (via Dfns — no direct RPC)

Prerequisites

Configuration

1

Clone and install

git clone https://github.com/dfns/dfns-solutions.git
cd dfns-solutions/bank-custody
2

Set up Auth0

Create an Auth0 application (Regular Web Application) and an API:
SettingValue
Allowed Callback URLshttp://localhost:3000/auth/callback
Allowed Logout URLshttp://localhost:3000
API Audiencehttps://bank-custody-api
Authorize your application to access the API (Application → APIs tab → toggle on). On the API settings, enable RBAC and Allow Offline Access.Create a Post-Login Action to include user info in the access token:
// Auth0 Action: "Add claims to token"
// IMPORTANT: The namespace MUST NOT be your Auth0 domain — Auth0 silently
// strips custom claims namespaced under its own domain.
exports.onExecutePostLogin = async (event, api) => {
  const namespace = "https://bank-custody.example.com";
  api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization?.roles || []);
  api.accessToken.setCustomClaim(`${namespace}/email`, event.user.email);
  api.accessToken.setCustomClaim(`${namespace}/name`, event.user.name);
};
The claims namespace cannot be your Auth0 tenant domain (e.g. https://your-tenant.auth0.com). Auth0 silently strips claims under its own domain. Use any other HTTPS URI.
To enable bank employee access, create a role called employee in Auth0 → User Management → Roles and assign it to your compliance officer user.
3

Configure environment variables

# Backend
cp backend/.env.example backend/.env
# Edit backend/.env with your Dfns and Auth0 credentials

# Frontend
cp frontend/.env.example frontend/.env
# Edit frontend/.env with your Auth0 credentials
Backend variables:
VariableDescription
DFNS_API_URLDfns API base URL (default: https://api.dfns.io)
DFNS_AUTH_TOKENService account auth token
DFNS_CRED_IDService account credential ID
DFNS_PRIVATE_KEYService account private key (PEM format)
AUTH0_DOMAINYour Auth0 tenant domain (e.g. your-tenant.auth0.com)
AUTH0_AUDIENCEAuth0 API audience identifier
APPROVAL_THRESHOLDTransfer amount in ETH requiring bank approval (default: 1000)
Frontend variables:
VariableDescription
AUTH0_SECRETA random string for session encryption
APP_BASE_URLFrontend URL (default: http://localhost:3000)
AUTH0_DOMAINYour Auth0 tenant domain
AUTH0_CLIENT_IDAuth0 application client ID
AUTH0_CLIENT_SECRETAuth0 application client secret
AUTH0_AUDIENCEAuth0 API audience identifier
NEXT_PUBLIC_API_URLBackend API URL (default: http://localhost:5001)
4

Start the backend

cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python app.py    # Starts on http://localhost:5001
Fiat accounts are auto-created for each customer on first login.
5

Start the frontend

cd frontend
npm install
npm run dev      # Starts on http://localhost:3000

Demo Walkthrough

The demo tells the story of a family banking with SecureBank: two parents (Alice and Bob) and their kid (Charlie). A bank compliance officer (Claire) oversees transfers.

Part 1 — Bank setup

  1. Create an Auth0 user for the compliance officer (e.g. compliance@yourbank.com)
  2. Assign the employee role to this user in Auth0 → User Management → Roles
  3. Set APPROVAL_THRESHOLD=0.05 in backend/.env so you can test the approval flow with small amounts

Part 2 — Create the family

Create three Auth0 users — they register automatically in the app on first login:
UserEmailRole
Parent 1 (Alice)alice@example.comCustomer
Parent 2 (Bob)bob@example.comCustomer
Kid (Charlie)charlie@example.comCustomer

Part 3 — Alice sets up wallets

Log in as Alice:
  1. Create wallets — click ”+ New crypto account” twice: “Alice’s Wallet” and “Charlie’s Wallet” (both Ethereum Sepolia)
  2. Fund both wallets using a Sepolia faucet
  3. Share access via the Family page:
    • Share “Alice’s Wallet” with Bob → View + Transfer
    • Share “Charlie’s Wallet” with Bob → View + Transfer
    • Share “Charlie’s Wallet” with Charlie → View + Transfer, approval threshold: 0.01 ETH

Part 4 — Bob sets up his wallet

Log in as Bob:
  1. Create a wallet — “Bob’s Wallet” (Ethereum Sepolia)
  2. Verify he can see Alice’s and Charlie’s wallets (marked “Shared”)
  3. Share “Bob’s Wallet” with Alice → View + Transfer

Part 5 — Charlie sends funds

Log in as Charlie:
  1. See “Charlie’s Wallet” in the dashboard (marked “Shared”)
  2. Send 0.001 ETH — goes through immediately (under his 0.01 ETH limit)
  3. Send 0.02 ETH — queued for bank approval (over his personal limit)

Part 6 — Bank employee reviews

Log in as the compliance officer:
  1. Go to the Admin section → Pending approvals
  2. See Charlie’s pending transfer
  3. Approve or Reject the transfer
  4. Approved transfers execute on-chain via Dfns

How It Works

Authentication Flow

The frontend uses Auth0 for authentication. On login, Auth0 issues a JWT access token containing custom claims (email, name, roles). The backend verifies this token against Auth0’s JWKS endpoint and auto-creates a local user record on first login.
Browser → Auth0 → JWT access token → Backend (verify + extract claims)

Wallet Operations

All wallet operations go through the backend’s Dfns service account:
OperationBackend actionDfns SDK call
Create walletPOST /api/walletswallets.create_wallet()
View balanceGET /api/wallets/:idwallets.get_wallet_assets()
TransferPOST /api/transferswallets.transfer_asset()
The backend converts ETH amounts to wei before sending to Dfns.

Approval Logic

Transfers are checked against two thresholds:
  1. Global threshold (APPROVAL_THRESHOLD env var) — applies to wallet owners
  2. Per-delegation limit (transfer_limit on delegations) — applies to delegated users, overrides the global threshold
If the transfer amount is at or above the applicable threshold, it’s queued in SQLite with status pending. A bank employee can then approve (which broadcasts via Dfns) or reject it.

Family Delegation

Wallet owners can grant two levels of access to family members:
PermissionWhat the grantee can do
ViewSee the wallet balance and transaction history
TransferView + initiate transfers (subject to per-person limits)
Delegations are stored in SQLite and enforced in the backend. The wallet owner can revoke access at any time.

API Reference

Customer endpoints (require Auth0 JWT)

MethodPathDescription
GET/api/walletsList own wallets, delegated wallets, and fiat accounts
POST/api/walletsCreate a new crypto wallet
GET/api/wallets/:idWallet detail with balance and transfer history
GET/api/transfersTransfer history
POST/api/transfersInitiate a transfer (may require approval)
GET/api/delegationsList delegations granted and received
POST/api/delegationsGrant family access (with optional transfer_limit)
DELETE/api/delegations/:idRevoke access

Employee endpoints (require Auth0 JWT + employee role)

MethodPathDescription
GET/api/admin/customersList all customers
GET/api/admin/customers/:id/walletsList a customer’s wallets
GET/api/admin/transfersPending transfers
POST/api/admin/transfers/:id/approveApprove and broadcast
POST/api/admin/transfers/:id/rejectReject transfer

Design Decisions

Dfns is the source of truth for wallet balances, addresses, and transaction status. SQLite stores what Dfns doesn’t know: user–wallet ownership, friendly names, delegations, and the approval queue. Service account pattern — the backend holds a single Dfns service account key. Customers never interact with Dfns directly. This is the “bank-as-custodian” model where the bank has full control over wallet operations. App-level approvals — transfer approval is handled in the application layer (SQLite queue + employee dashboard), not via Dfns policies. This keeps the architecture simple and lets the bank define custom rules (per-user limits, role-based access). A future blueprint could explore Dfns policies for on-chain governance where bank employees are registered as Dfns users. Mobile-ready API — pure REST, JWT Bearer auth, JSON responses. No server-side sessions or cookies required on the API side.

Adapting for Production

Different networks — change the network parameter when creating wallets (e.g., Ethereum, Polygon, Arbitrum). The Dfns SDK handles chain-specific details. Real fiat integration — replace the demo fiat accounts with a real banking API (e.g., Plaid, Open Banking) to show actual account balances. Multi-level approvals — extend the approval logic to support multiple approvers, approval chains, or time-delayed execution for high-value transfers. Dfns policies — for stricter governance, Dfns policies can enforce signing rules (e.g., approval quorums, velocity limits) independent of the application layer. This requires bank employees to be registered as Dfns users rather than using a single service account.
Last modified on March 6, 2026