Skip to main content

Accounts

Modular Accounts are smart contract wallets that serve as deposit addresses for users. They are deterministic, meaning the same user always gets the same address across all supported chains.

Get User Account

Retrieve the Modular Account details for a user. Endpoint: GET /api/users/:userId/account Authentication: Required (API Key)

Request

Headers:
x-api-key: your_api_key
Path Parameters:
ParameterTypeDescription
userIdstringUser identifier

Response

Status: 200 OK
{
  "userId": "user-123",
  "modularAccountAddress": "0xABC...123",
  "canonicalChainId": 8453,
  "canonicalChainKey": "base",
  "supportedChains": [
    8453,
    1,
    137,
    42161,
    43114
  ]
}
Response Fields:
FieldTypeDescription
userIdstringUser identifier
modularAccountAddressstringSmart contract wallet address (same on all chains)
canonicalChainIdnumberChain where account was first provisioned
canonicalChainKeystringHuman-readable canonical chain identifier
supportedChainsnumber[]List of chain IDs where this account exists

Example

curl https://api.bridgfy.com/api/users/user-abc-123/account \
  -H "x-api-key: bfy_your_api_key"
Response:
{
  "userId": "user-abc-123",
  "modularAccountAddress": "0x1234567890123456789012345678901234567890",
  "canonicalChainId": 8453,
  "canonicalChainKey": "base",
  "supportedChains": [8453, 1, 137, 42161, 43114]
}

Error Responses

Not Found Status: 404 Not Found
{
  "statusCode": 404,
  "message": "Account not found for user: user-123",
  "error": "Not Found"
}
Note: Accounts are created automatically when you create a Deposit Intent. If a user has no account, they haven’t created a Deposit Intent yet. Unauthorized Status: 401 Unauthorized
{
  "statusCode": 401,
  "message": "Invalid API key",
  "error": "Unauthorized"
}

Create Account (Advanced)

Provision a Modular Account for a user without creating a Deposit Intent. Endpoint: POST /api/accounts Authentication: Required (API Key) Note: Most integrations don’t need this endpoint. Accounts are automatically created when you create a Deposit Intent via POST /api/deposit-intents.

Request

Headers:
x-api-key: your_api_key
Content-Type: application/json
Body:
{
  "ownerAddress": "string",
  "chainKey": "string",
  "label": "string"
}
Parameters:
FieldTypeRequiredDescription
ownerAddressstringYesUser identifier (typically same as userId)
chainKeystringYesChain to provision on (e.g., “base”, “ethereum”)
labelstringNoOptional label for the account

Response

Status: 201 Created
{
  "label": "My Account",
  "status": "ready",
  "chain": "base",
  "modularAccountAddress": "0xABC...123",
  "owner": "user-123",
  "canonicalChain": {
    "chainId": 8453,
    "chainKey": "base"
  }
}

Example

curl -X POST https://api.bridgfy.com/api/accounts \
  -H "x-api-key: bfy_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "ownerAddress": "user-abc-123",
    "chainKey": "base",
    "label": "My Account"
  }'
Note: This endpoint is rarely needed. Use POST /api/deposit-intents instead, which creates both an account and a deposit intent in one call.

Understanding Modular Accounts

What is a Modular Account?

A Modular Account is a smart contract wallet (ERC-4337/ERC-6900 compatible) that:
  • Acts as the deposit address for a user
  • Is controlled by Bridgfy on behalf of the user
  • Executes cross-chain transfers automatically based on Deposit Intents
  • Has the same address on all supported EVM chains
Key features:
  • Deterministic: Same address for a user across all chains
  • Non-custodial: Only acts on predefined Deposit Intents
  • Secure: Cannot be controlled by anyone except the Bridgfy system

Account Lifecycle

Flow:
  1. You create a Deposit Intent for a user
  2. If the user doesn’t have an account, one is provisioned automatically
  3. The account is deployed on the canonical chain (typically Base)
  4. The same address is available on all supported chains
  5. Future Deposit Intents for the same user reuse the same account

Address Determinism

The Modular Account address is derived from:
  • User ID
  • Bridgfy’s account factory configuration
  • Chain-specific salt
This means:
  • Same userId → Same address
  • Address is predictable and stable
  • Can be safely stored and reused
  • Works across all supported chains

Supported Chains

Modular Accounts work on all Bridgfy-supported chains:
ChainChain IDKey
Base8453base
Ethereum1ethereum
Polygon PoS137polygon
Arbitrum One42161arbitrum
Avalanche C-Chain43114avalanche
The account has the same address on all these chains.

Canonical Chain

The canonicalChainId is the chain where the account was first provisioned. This is typically:
  • Base (8453) for most accounts
  • Or the first chain where a Deposit Intent was created
Why it matters:
  • Determines where the account is “anchored”
  • May affect gas costs for first-time setup
  • Has no impact on functionality (account works the same on all chains)

Use Cases

1. Check if User Has Account

Before showing deposit options:
async function hasAccount(userId) {
  try {
    const account = await fetch(
      `https://api.bridgfy.com/api/users/${userId}/account`,
      { headers: { 'x-api-key': apiKey } }
    );
    return account.ok;
  } catch (error) {
    return false;
  }
}

2. Display Deposit Address

Show users their deposit address for a specific chain:
async function getDepositAddress(userId) {
  const account = await fetch(
    `https://api.bridgfy.com/api/users/${userId}/account`,
    { headers: { 'x-api-key': apiKey } }
  ).then(r => r.json());
  
  return account.modularAccountAddress;
}

// Usage
const address = await getDepositAddress('user-123');
showQRCode(address);
showText(`Send funds to: ${address}`);

3. Verify Account Exists on Chain

The account address is the same on all chains, but you can verify it’s the same:
async function verifyAccountOnChain(userId, chainId) {
  const account = await getAccount(userId);
  
  if (!account.supportedChains.includes(chainId)) {
    console.warn(`Account not confirmed on chain ${chainId}`);
  }
  
  return account.modularAccountAddress;
}
Show users their account on a block explorer:
function getExplorerUrl(address, chainKey) {
  const explorers = {
    base: `https://basescan.org/address/${address}`,
    ethereum: `https://etherscan.io/address/${address}`,
    polygon: `https://polygonscan.com/address/${address}`,
    arbitrum: `https://arbiscan.io/address/${address}`,
    avalanche: `https://snowtrace.io/address/${address}`
  };
  
  return explorers[chainKey];
}

// Usage
const account = await getAccount(userId);
const explorerUrl = getExplorerUrl(
  account.modularAccountAddress,
  account.canonicalChainKey
);

Best Practices

Don’t Call Unnecessarily

Accounts are created automatically when you create Deposit Intents. In most cases, you don’t need to:
  • Call POST /api/accounts (use POST /api/deposit-intents instead)
  • Call GET /api/users/:userId/account before creating an intent
When to use account endpoints:
  • Displaying account info in user profile
  • Verifying an account exists before advanced operations
  • Building account management features

Store the Address

Once you have a user’s deposit address:
  1. Store it in your database
  2. Reuse it for displaying to users
  3. Don’t fetch repeatedly - the address never changes
// Good: Store in database
const intent = await createDepositIntent({...});
await db.users.update(userId, {
  depositAddress: intent.depositAddress
});

// Later: Use stored address
const address = await db.users.get(userId).depositAddress;
showDepositAddress(address);

Handle Missing Accounts

If a user doesn’t have an account yet:
async function getOrCreateAccount(userId, targetChainId, targetTokenAddress, targetAddress) {
  try {
    // Try to get existing account
    const account = await getAccount(userId);
    return account;
  } catch (error) {
    if (error.status === 404) {
      // No account - create a Deposit Intent (creates account automatically)
      const intent = await createDepositIntent({
        userId,
        targetChainId,
        targetTokenAddress,
        targetAddress
      });
      
      return {
        userId,
        modularAccountAddress: intent.depositAddress,
        canonicalChainId: targetChainId,
        supportedChains: [targetChainId]
      };
    }
    throw error;
  }
}

Security Considerations

Important:
  • The Modular Account is controlled by Bridgfy, not the user
  • It only executes transfers based on Deposit Intents
  • Users cannot manually withdraw funds from the account
  • Funds are automatically routed according to the user’s intent
What this means:
  • Don’t tell users they can “withdraw” from this address
  • Explain it’s a “deposit-only” address for cross-chain routing
  • Make it clear funds are automatically forwarded to their destination

Display Best Practices

When showing the deposit address to users:
function DepositAddressDisplay({ account }) {
  return (
    <div className="deposit-info">
      <h3>Your Deposit Address</h3>
      <p>Send funds to this address on any supported chain:</p>
      
      <div className="address-display">
        <code>{account.modularAccountAddress}</code>
        <button onClick={() => copyToClipboard(account.modularAccountAddress)}>
          Copy
        </button>
      </div>
      
      <div className="qr-code">
        <QRCode value={account.modularAccountAddress} />
      </div>
      
      <div className="supported-chains">
        <p>Supported chains:</p>
        <ul>
          {account.supportedChains.map(chainId => (
            <li key={chainId}>{getChainName(chainId)}</li>
          ))}
        </ul>
      </div>
      
      <div className="warning">
        ⚠️ Funds sent to this address will be automatically routed to your 
        configured destination. Do not send funds here unless you have set 
        up a Deposit Intent.
      </div>
    </div>
  );
}

Next Steps