> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pods.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# EOA Integration

> Integrate an EOA with Pods: create an ERC-4337 smart account, fund it, and deposit into a yield strategy with a sponsored userOperation

This example walks an EOA holder through depositing into a Pods yield strategy via an **ERC-4337 smart account**
(a Gnosis Safe with the Safe 4337 module). Instead of signing each on-chain transaction with the EOA, you create a
smart account owned by the EOA, fund it once, then deposit by **signing a single userOperation** that the bundler
executes — with gas sponsored by a paymaster.

## Overview

This example covers:

* Creating a smart account for an EOA with `POST /smart-account`
* Waiting for the smart account to deploy on-chain
* Funding the smart account from the EOA (a standard ERC-20 transfer)
* Generating a deposit `userOperation` with `GET /strategies/:id/bytecode?output=userOperation`
* Signing the userOperation (Safe 4337 `SafeOp` EIP-712)
* Propagating it with `POST /aa/send-user-operation`

<Info>
  The signer key is the smart account's **owner**. The EOA you use to sign the userOperation must be the same `owner`
  you pass to `POST /smart-account`, or the Safe 4337 module rejects the signature (`AA24`).
</Info>

## Prerequisites

* Node.js 18+
* Pods API key
* An EOA (private key) with USDC on Polygon to deposit
* Basic understanding of async/await

<Note>
  Gas is **sponsored**. The deposit userOperation is paid by a paymaster, so the smart account only needs the asset you
  are depositing (USDC) — it does **not** need native MATIC for gas.
</Note>

## Installation

<Tabs>
  <Tab title="ethers.js">
    ```bash theme={null}
    npm install axios ethers
    ```
  </Tab>

  <Tab title="viem">
    ```bash theme={null}
    npm install axios viem
    ```
  </Tab>
</Tabs>

## Environment Variables

Create a `.env` file:

```bash theme={null}
PODS_API_KEY=your-api-key
PRIVATE_KEY=0x...                 # the EOA that owns the smart account and signs the userOperation
POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/your-key
```

<Warning>
  Never commit your `.env` file to version control. Add it to `.gitignore`.
</Warning>

## Step-by-Step Walkthrough

### 1. Initialize clients

```javascript theme={null}
import axios from 'axios'

const CHAIN_ID = 137 // Polygon
const STRATEGY_ID = 'aave-usdc-polygon'
const USDC = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' // USDC on Polygon
const AMOUNT = '1000000' // raw units — USDC has 6 decimals, so 1000000 = 1 USDC

const pods = axios.create({
  baseURL: 'https://api.pods.finance',
  headers: {
    'x-api-key': process.env.PODS_API_KEY,
    'Content-Type': 'application/json'
  }
})
```

<Tabs>
  <Tab title="ethers.js">
    ```javascript theme={null}
    import { ethers } from 'ethers'

    const provider = new ethers.JsonRpcProvider(process.env.POLYGON_RPC_URL)
    const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider)

    const owner = signer.address // the EOA — also the smart account owner
    console.log('Owner (EOA):', owner)
    ```
  </Tab>

  <Tab title="viem">
    ```javascript theme={null}
    import { createPublicClient, createWalletClient, http } from 'viem'
    import { polygon } from 'viem/chains'
    import { privateKeyToAccount } from 'viem/accounts'

    const account = privateKeyToAccount(process.env.PRIVATE_KEY)
    const publicClient = createPublicClient({ chain: polygon, transport: http(process.env.POLYGON_RPC_URL) })
    const walletClient = createWalletClient({ account, chain: polygon, transport: http(process.env.POLYGON_RPC_URL) })

    const owner = account.address // the EOA — also the smart account owner
    console.log('Owner (EOA):', owner)
    ```
  </Tab>
</Tabs>

### 2. Create the smart account

Create a smart account owned by the EOA. The address is **deterministic** for an `owner` + `chainId`, so this call is
idempotent — repeated calls return the same record.

```javascript theme={null}
const { data: smartAccount } = await pods.post('/smart-account', {
  owner,
  chainId: CHAIN_ID
})

const smartAccountAddress = smartAccount.address
console.log('Smart account:', smartAccountAddress)
```

Response:

```json theme={null}
{
  "address": "0x4F380a141607935c747725a07A97a146CF512755",
  "owner": "0xb794F5eA0ba39494cE839613fffBA74279579268",
  "chainId": 137
}
```

<Note>
  The endpoint returns `200` with the record immediately, while the Safe is deployed **in the background**. The next
  step waits for that deployment to land on-chain.
</Note>

### 3. Wait for on-chain deployment

The smart account must have code on-chain before you can build a userOperation for it. Poll until it's deployed.

<Warning>
  Skip this and Step 5 fails: building a `userOperation` for a not-yet-deployed smart account throws
  `Could not create a new SmartAccount without an owner`.
</Warning>

<Tabs>
  <Tab title="ethers.js">
    ```javascript theme={null}
    async function waitForDeployment (address, { tries = 30, delayMs = 5000 } = {}) {
      for (let i = 0; i < tries; i++) {
        const code = await provider.getCode(address)
        if (code && code !== '0x') return true
        await new Promise(resolve => setTimeout(resolve, delayMs))
      }
      throw new Error(`Smart account ${address} was not deployed in time`)
    }

    await waitForDeployment(smartAccountAddress)
    console.log('Smart account deployed')
    ```
  </Tab>

  <Tab title="viem">
    ```javascript theme={null}
    async function waitForDeployment (address, { tries = 30, delayMs = 5000 } = {}) {
      for (let i = 0; i < tries; i++) {
        const code = await publicClient.getBytecode({ address })
        if (code && code !== '0x') return true
        await new Promise(resolve => setTimeout(resolve, delayMs))
      }
      throw new Error(`Smart account ${address} was not deployed in time`)
    }

    await waitForDeployment(smartAccountAddress)
    console.log('Smart account deployed')
    ```
  </Tab>
</Tabs>

### 4. Fund the smart account

Move the asset you want to deposit from the EOA to the smart account. This is a plain ERC-20 `transfer()` — there is no
Pods endpoint for it.

<Tabs>
  <Tab title="ethers.js">
    ```javascript theme={null}
    const ERC20_ABI = ['function transfer(address to, uint256 amount) returns (bool)']
    const usdc = new ethers.Contract(USDC, ERC20_ABI, signer)

    const fundTx = await usdc.transfer(smartAccountAddress, AMOUNT)
    console.log('Funding tx:', fundTx.hash)
    await fundTx.wait()
    console.log('Smart account funded')
    ```
  </Tab>

  <Tab title="viem">
    ```javascript theme={null}
    const ERC20_ABI = [{
      type: 'function',
      name: 'transfer',
      stateMutability: 'nonpayable',
      inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],
      outputs: [{ type: 'bool' }]
    }]

    const fundHash = await walletClient.writeContract({
      address: USDC,
      abi: ERC20_ABI,
      functionName: 'transfer',
      args: [smartAccountAddress, BigInt(AMOUNT)]
    })
    console.log('Funding tx:', fundHash)
    await publicClient.waitForTransactionReceipt({ hash: fundHash })
    console.log('Smart account funded')
    ```
  </Tab>
</Tabs>

<Note>
  Only the deposit asset (USDC) is required — gas for the deposit userOperation is sponsored by a paymaster.
</Note>

### 5. Request the deposit userOperation

Ask the API for the lend bytecode as a fully-populated (but unsigned) userOperation, using the **smart account** as the
wallet. Gas limits and `paymasterAndData` are filled in for you.

```javascript theme={null}
const { data: result } = await pods.get(`/strategies/${STRATEGY_ID}/bytecode`, {
  params: {
    action: 'lend',
    wallet: smartAccountAddress,
    amount: AMOUNT,
    output: 'userOperation'
  }
})

const userOperation = result.userOperation
console.log('Action id:', result.id) // keep for tracking
```

The `userOperation` contains `sender`, `nonce`, `initCode`, `callData`, the gas fields, `paymasterAndData`, and an empty
`signature` you fill in next.

### 6. Sign the userOperation (SafeOp)

<Info>
  A Safe with the 4337 module validates the userOperation against the module's **`SafeOp`** EIP-712 hash — **not** the
  vanilla ERC-4337 `UserOperation` hash. The EIP-712 `verifyingContract` is the Safe 4337 module, and the packed
  signature is `12 zero bytes` (`validAfter` + `validUntil`) followed by the 65-byte ECDSA signature.
</Info>

<Tabs>
  <Tab title="ethers.js">
    ```javascript theme={null}
    // Canonical Safe 4337 (EntryPoint v0.6) addresses — identical on every chain
    const SAFE_4337_MODULE_ADDRESS = '0xa581c4A4DB7175302464fF3C06380BC3270b4037'
    const ENTRYPOINT_ADDRESS = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'

    async function signUserOperation (userOperation, signer, chainId) {
      const domain = { chainId, verifyingContract: SAFE_4337_MODULE_ADDRESS }
      const types = {
        SafeOp: [
          { type: 'address', name: 'safe' },
          { type: 'uint256', name: 'nonce' },
          { type: 'bytes', name: 'initCode' },
          { type: 'bytes', name: 'callData' },
          { type: 'uint256', name: 'callGasLimit' },
          { type: 'uint256', name: 'verificationGasLimit' },
          { type: 'uint256', name: 'preVerificationGas' },
          { type: 'uint256', name: 'maxFeePerGas' },
          { type: 'uint256', name: 'maxPriorityFeePerGas' },
          { type: 'bytes', name: 'paymasterAndData' },
          { type: 'uint48', name: 'validAfter' },
          { type: 'uint48', name: 'validUntil' },
          { type: 'address', name: 'entryPoint' }
        ]
      }
      const message = {
        safe: userOperation.sender,
        nonce: userOperation.nonce,
        initCode: userOperation.initCode,
        callData: userOperation.callData,
        callGasLimit: userOperation.callGasLimit,
        verificationGasLimit: userOperation.verificationGasLimit,
        preVerificationGas: userOperation.preVerificationGas,
        maxFeePerGas: userOperation.maxFeePerGas,
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas,
        paymasterAndData: userOperation.paymasterAndData,
        validAfter: '0x000000000000',
        validUntil: '0x000000000000',
        entryPoint: ENTRYPOINT_ADDRESS
      }

      const signature = await signer.signTypedData(domain, types, message)
      // 6 bytes validAfter + 6 bytes validUntil (both zero) + the 65-byte signature
      return '0x000000000000000000000000' + signature.slice(2)
    }

    userOperation.signature = await signUserOperation(userOperation, signer, CHAIN_ID)
    ```
  </Tab>

  <Tab title="viem">
    ```javascript theme={null}
    // Canonical Safe 4337 (EntryPoint v0.6) addresses — identical on every chain
    const SAFE_4337_MODULE_ADDRESS = '0xa581c4A4DB7175302464fF3C06380BC3270b4037'
    const ENTRYPOINT_ADDRESS = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'

    async function signUserOperation (userOperation, walletClient, chainId) {
      const signature = await walletClient.signTypedData({
        domain: { chainId, verifyingContract: SAFE_4337_MODULE_ADDRESS },
        types: {
          SafeOp: [
            { type: 'address', name: 'safe' },
            { type: 'uint256', name: 'nonce' },
            { type: 'bytes', name: 'initCode' },
            { type: 'bytes', name: 'callData' },
            { type: 'uint256', name: 'callGasLimit' },
            { type: 'uint256', name: 'verificationGasLimit' },
            { type: 'uint256', name: 'preVerificationGas' },
            { type: 'uint256', name: 'maxFeePerGas' },
            { type: 'uint256', name: 'maxPriorityFeePerGas' },
            { type: 'bytes', name: 'paymasterAndData' },
            { type: 'uint48', name: 'validAfter' },
            { type: 'uint48', name: 'validUntil' },
            { type: 'address', name: 'entryPoint' }
          ]
        },
        primaryType: 'SafeOp',
        message: {
          safe: userOperation.sender,
          nonce: BigInt(userOperation.nonce),
          initCode: userOperation.initCode,
          callData: userOperation.callData,
          callGasLimit: BigInt(userOperation.callGasLimit),
          verificationGasLimit: BigInt(userOperation.verificationGasLimit),
          preVerificationGas: BigInt(userOperation.preVerificationGas),
          maxFeePerGas: BigInt(userOperation.maxFeePerGas),
          maxPriorityFeePerGas: BigInt(userOperation.maxPriorityFeePerGas),
          paymasterAndData: userOperation.paymasterAndData,
          validAfter: 0,
          validUntil: 0,
          entryPoint: ENTRYPOINT_ADDRESS
        }
      })
      // 6 bytes validAfter + 6 bytes validUntil (both zero) + the 65-byte signature
      return '0x000000000000000000000000' + signature.slice(2)
    }

    userOperation.signature = await signUserOperation(userOperation, walletClient, CHAIN_ID)
    ```
  </Tab>
</Tabs>

### 7. Send the userOperation

Propagate the signed userOperation to the bundler. The endpoint returns the `userOpHash`.

```javascript theme={null}
const { data: sent } = await pods.post('/aa/send-user-operation', {
  chainId: CHAIN_ID,
  userOperation
})

console.log('userOpHash:', sent.userOpHash)
```

<Note>
  Numeric userOperation fields may be decimal or `0x`-hex strings — the endpoint normalizes them, and the SafeOp
  signature hashes the integer values, so signing the fields exactly as returned by Step 5 is correct.
</Note>

### 8. Track the deposit (optional)

Use the `id` from Step 5 to poll the action until it reaches a terminal state.

```javascript theme={null}
async function waitForDeposit (id) {
  const TERMINAL = ['SUCCESS', 'FAILED', 'REFUNDED', 'EXPIRED']

  while (true) {
    const { data: action } = await pods.get(`/actions/${id}`)
    console.log(`Status: ${action.status} (${action.transactions.length} tx)`)

    if (TERMINAL.includes(action.status)) return action

    await new Promise(resolve => setTimeout(resolve, 5000))
  }
}

const finalAction = await waitForDeposit(result.id)
finalAction.transactions.forEach(tx => {
  console.log(`  ${tx.chain}: ${tx.txHash} (${tx.status})`)
})
```

## Error Handling

```javascript theme={null}
try {
  // Your code here
} catch (error) {
  if (error.response?.data?.error) {
    const apiError = error.response.data.error
    console.error('API Error:', apiError.code)
    console.error('Message:', apiError.message)

    if (apiError.code === 'USER_OPERATION_REJECTED') {
      // The bundler rejected the userOp — the message carries the AA reason code:
      //   AA24 = signature (is the signer the smart account owner? is it a SafeOp signature?)
      //   AA25 = nonce      (stale userOp — re-fetch it in Step 5)
      //   AA21 / AA31 = prefund / paymaster (not a signing problem)
      console.log('Bundler rejected the user operation:', apiError.message)
    }
  } else {
    console.error('Unexpected error:', error.message)
  }
}
```

## Expected Output

```
Owner (EOA): 0xb794F5eA0ba39494cE839613fffBA74279579268

1️⃣  Creating smart account...
✅ Smart account: 0x4F380a141607935c747725a07A97a146CF512755

2️⃣  Waiting for on-chain deployment...
✅ Smart account deployed

3️⃣  Funding smart account (1 USDC)...
   Funding tx: 0x123...
✅ Smart account funded

4️⃣  Requesting deposit userOperation...
✅ Action id: 65f3a1c8e9b2d4f8a7c1d2e3

5️⃣  Signing userOperation (SafeOp)...
✅ Signed

6️⃣  Sending to bundler...
✅ userOpHash: 0xabc...

🎉 Deposit submitted via smart account!
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Strategy Deposit (EOA)" href="/examples/javascript/strategy-deposit">
    The same deposit flow signing directly with an EOA
  </Card>

  <Card title="Check Positions" href="/guides/yield/check-positions">
    Track the smart account's positions
  </Card>

  <Card title="Strategy Withdrawal" href="/examples/javascript/strategy-withdraw">
    Withdraw from strategies, including cross-chain
  </Card>

  <Card title="API Reference" href="/api-reference/introduction">
    View complete API documentation
  </Card>
</CardGroup>
