# Onchain DCA permission

Build a delegated transaction execution system where a backend agent autonomously performs Dollar-Cost Averaging (DCA) token swaps on behalf of users. This recipe combines Openfort embedded wallets with Calibur smart accounts (EIP-7702 + EIP-4337) on Base Sepolia to demonstrate time-limited, non-custodial agent permissions with gas-sponsored transactions.

:::tip
You'll build a Next.js app where users authenticate, create an embedded wallet, and enable automated DCA purchases. A backend agent wallet gets a 5-minute session key on the user's Calibur smart account, and a cron job executes gas-sponsored swaps every minute — all without the user needing to hold ETH.
:::

<HoverCardLink
  title="View Sample Code"
  subtitle="GitHub Repository"
  description="Complete source code for the wallet permissions recipe with delegated DCA agent execution on Base Sepolia."
  href="https://github.com/openfort-xyz/recipes-hub/tree/main/agent-permissions"
  img={{
  src: "/img/icons/github-icon.svg",
  alt: "GitHub Icon",
  className: "rounded-none",
}}
  color="#333"
  external
/>

## Getting started

:::note
* Node.js 18+
* [Openfort account](https://dashboard.openfort.io) with Shield credentials
* [Upstash Redis](https://console.upstash.com) database
* [Vercel account](https://vercel.com) (for cron job deployment in production)
:::

::::steps
### Set up your project

Clone the recipe and install dependencies:

```bash
pnpx gitpick openfort-xyz/recipes-hub/tree/main/agent-permissions openfort-agent-permissions
cd openfort-agent-permissions
pnpm i
```

The project is a Next.js 15 application with API routes handling the DCA logic:

```text
agent-permissions/
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   ├── airdrop/route.ts         # Sends test USDC from backend wallet
│   │   │   └── dca/
│   │   │       ├── route.ts             # Read/write DCA config, create agent wallet
│   │   │       └── execute/route.ts     # Cron-triggered DCA swap execution
│   │   └── page.tsx                     # Client entry point
│   ├── components/
│   │   ├── Providers.tsx                # Wagmi + Openfort + React Query
│   │   └── cards/
│   │       ├── auth.tsx                 # Email OTP login
│   │       ├── wallets.tsx              # Wallet creation with passkey recovery
│   │       └── balance.tsx              # Dashboard: balances, DCA controls
│   └── lib/
│       ├── calibur/index.ts             # Calibur smart account + session keys
│       ├── dcaStore.ts                  # Upstash Redis persistence
│       └── auth.ts                      # Openfort IAM session validation
├── vercel.json                          # Cron schedule (every minute)
└── .env.example
```

### Configure your Openfort credentials

Set up your project in the [Openfort Dashboard](https://dashboard.openfort.io):

1. Create an account or sign in
2. Get your API keys:
   * **Publishable Key** (`pk_test_...`) — used by the frontend
   * **Secret Key** (`sk_test_...`) — used by the backend API routes
3. Set up **Shield** for embedded wallets and note the **Shield Publishable Key**
4. Create a **Policy** for gas sponsorship and note the **Policy ID** (`pol_...`)
5. Create a **Backend Wallet** for the airdrop faucet — note the **Wallet ID** (`acc_...`) and **Wallet Secret Key**

### Set up Upstash Redis

The recipe uses [Upstash Redis](https://console.upstash.com) to persist DCA configurations and track active agents across cron executions.

1. Go to [Upstash Console](https://console.upstash.com)
2. Create a new Redis database
3. Copy the **REST URL** and **REST Token** from the database details

The DCA store uses a simple key-value model — each user's config is stored at `dca:<address>` and a Redis set `dca:agents` tracks all active users:

```typescript
// src/lib/dcaStore.ts
export const dcaStore = {
  async get(key: string): Promise<DcaConfig | null> {
    return getRedis().get<DcaConfig>(`${PREFIX}${key}`)
  },
  async set(key: string, value: DcaConfig): Promise<void> {
    const redis = getRedis()
    await redis.set(`${PREFIX}${key}`, value)
    await redis.sadd(AGENTS_SET, key)  // Track active agents
  },
  async listAgents(): Promise<string[]> {
    return getRedis().smembers(AGENTS_SET)
  },
}
```

### Generate a cron secret

The `/api/dca/execute` endpoint is called by a Vercel cron job every minute. It needs a secret to prevent unauthorized access:

```bash
# Generate a random secret
openssl rand -base64 32
```

Save this value — you'll use it as `CRON_SECRET` in the next step.

The `vercel.json` configures the cron schedule:

```json
{
  "crons": [
    {
      "path": "/api/dca/execute",
      "schedule": "* * * * *"
    }
  ]
}
```

### Configure your environment

Create `.env.local` from the template:

```bash
cp .env.example .env.local
```

Fill in all values:

```bash
# Openfort Configuration (from https://dashboard.openfort.io)
NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_OPENFORT_SHIELD_PUBLISHABLE_KEY=your-shield-publishable-key
NEXT_PUBLIC_POLICY_ID=pol_...

# Server-side Openfort
OPENFORT_SECRET_KEY=sk_test_...
OPENFORT_WALLET_SECRET_KEY=              # Backend wallet encryption key
OPENFORT_BACKEND_WALLET_ID=              # acc_xxx — backend wallet for airdrops

# Vercel Cron (secures /api/dca/execute)
CRON_SECRET=your-generated-secret

# Upstash Redis (for persistent DCA store)
UPSTASH_REDIS_REST_URL=https://...upstash.io
UPSTASH_REDIS_REST_TOKEN=AX...
```

### Understand the Openfort integration

The frontend wraps the app in Openfort's provider stack for authentication and wallet management. The wallet is configured as a `DELEGATED_ACCOUNT` (EIP-7702) so users get a Calibur smart account:

```tsx
// src/components/Providers.tsx
<OpenfortProvider
  publishableKey={process.env.NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY!}
  walletConfig={{
    shieldPublishableKey: process.env.NEXT_PUBLIC_OPENFORT_SHIELD_PUBLISHABLE_KEY!,
    ethereum: {
      accountType: AccountTypeEnum.EOA,
      ethereumFeeSponsorshipId: process.env.NEXT_PUBLIC_POLICY_ID,
    },
  }}
>
  {children}
</OpenfortProvider>
```

Wallet creation uses `DELEGATED_ACCOUNT` type so the EOA delegates to the Calibur implementation, enabling session key management:

```typescript
// src/components/cards/wallets.tsx
await createWallet({
  accountType: AccountTypeEnum.DELEGATED_ACCOUNT,
  recovery:
    recoveryMethod === RecoveryMethod.PASSKEY
      ? { recoveryMethod: RecoveryMethod.PASSKEY }
      : { recoveryMethod: RecoveryMethod.PASSWORD, password },
})
```

### Understand the session key flow

When a user enables DCA, the backend creates an agent wallet and the frontend registers it as a session key on the user's Calibur smart account. Here's the key Calibur integration:

```typescript
// src/lib/calibur/index.ts

// Keys are identified by keccak256(abi.encode(keyType, keccak256(publicKey)))
export function hashKey(key: CaliburKey): Hex {
  return keccak256(
    encodeAbiParameters(
      parseAbiParameters('uint8, bytes32'),
      [key.keyType, keccak256(key.publicKey)]
    )
  )
}

// Settings are bit-packed: isAdmin (bit 200), expiration (bits 160-199), hook (bits 0-159)
export function packSettings(settings: KeySettings): bigint {
  const admin = settings.isAdmin ? BigInt(1) : BigInt(0)
  const exp = BigInt(settings.expiration)
  const hook = BigInt(getAddress(settings.hook))
  return (admin << BigInt(200)) | (exp << BigInt(160)) | hook
}
```

The frontend registers the agent key and sets a 5-minute expiration in a single batched transaction:

```typescript
// src/components/cards/balance.tsx
const agentAddr = getAddress(data.agentAddress)
const agentKey = {
  keyType: KeyType.Secp256k1,
  publicKey: padHex(agentAddr, { size: 32 }),
}
const expiration = Math.floor(Date.now() / 1000) + DCA_DURATION_SECONDS // 5 minutes

const registerCall = encodeRegisterKey(agentKey)
const keyHash = hashKey(agentKey)
const updateCall = encodeUpdateKeySettings(keyHash, {
  isAdmin: false,
  expiration,
  hook: ZERO_ADDRESS,
})

// Both calls execute atomically on the user's own Calibur account
const txData = encodeExecute([registerCall, updateCall])
await sendTransactionAsync({ to: address, data: txData })
```

The cron job then creates a session account for the agent and submits gas-sponsored UserOperations:

```typescript
// src/app/api/dca/execute/route.ts
const sessionAccount = await createCaliburSessionAccount({
  client: viemClient,
  signer: viemAccount,       // The agent's Openfort backend wallet
  accountAddress: userAddress, // Operates on the user's Calibur account
  keyHash,                    // Agent's registered key hash
})

const bundlerClient = createBundlerClient({
  account: sessionAccount,
  paymaster: paymasterClient,
  client: viemClient,
  paymasterContext: { policyId: process.env.NEXT_PUBLIC_POLICY_ID },
  transport: rpcTransport,
})

// Execute the DCA swap as a UserOperation
const userOpHash = await bundlerClient.sendUserOperation({
  account: sessionAccount,
  calls: [
    { to: USDC_ADDRESS, value: BigInt(0), data: transferCalldata },     // Send USDC
    { to: MOCK_ERC20_ADDRESS, value: BigInt(0), data: mintCalldata },   // Receive mock WETH
  ],
})
```

### Run the application

```bash
pnpm dev
```

Visit [http://localhost:3000](http://localhost:3000). The UI guides you through:

1. **Sign in** with email OTP
2. **Create a wallet** with passkey recovery
3. **Request an airdrop** to get test USDC
4. **Enable DCA** — this registers the agent key on-chain and starts automated purchases

:::note
For production deployment with cron jobs, deploy to Vercel. The `vercel.json` configures the cron schedule to run `/api/dca/execute` every minute. Locally, you can trigger execution manually via `POST /api/dca/execute`.
:::
::::

## How it works

The recipe implements a 6-step delegated execution flow:

:::steps
### Authentication

The user authenticates via email OTP using Openfort IAM. The backend validates sessions by calling `openfort.iam.getSession()` and verifies address ownership via `openfort.accounts.evm.embedded.list()`.

### Agent provisioning

When the user enables DCA, `POST /api/dca` creates a new Openfort backend wallet to act as the agent:

```typescript
const agent = await createBackendWallet({ chainType: 'EVM' })
```

The agent's address and ID are stored in Upstash Redis alongside the DCA configuration.

### Key registration

The frontend registers the agent's address as a Secp256k1 session key on the user's Calibur smart account with a 5-minute expiration. The key is non-admin — it can only execute transactions within its permission window.

### Cron execution

A Vercel cron job (`/api/dca/execute`) runs every minute. It discovers active agents from the Redis `dca:agents` set, verifies each agent's on-chain key is still valid (not expired), and skips any that have been revoked.

### DCA swap

For each active agent, the cron creates a `CaliburSessionAccount` that signs UserOperations with the agent's key hash. The bundler client submits the swap as a gas-sponsored UserOperation — transferring USDC and minting mock WETH at a simulated price.

### Gas sponsorship

All transactions are gas-sponsored via Openfort's paymaster using the configured policy ID. Users never need ETH — the paymaster covers gas for both the initial key registration (from the embedded wallet) and all subsequent DCA executions (from the agent's session key).
:::

### Calibur key model

Keys are registered on the user's Calibur account at `0x000000009b1d0af20d8c6d0a44e162d11f9b8f00` and identified by `keccak256(abi.encode(keyType, keccak256(publicKey)))`.

Key settings are bit-packed into a `uint256`:

* `isAdmin` (bit 200) — full account control
* `expiration` (bits 160-199) — unix timestamp, 0 = never expires
* `hook` (bits 0-159) — optional validator contract

The DCA agent uses a Secp256k1 key with `isAdmin: false` and a 5-minute expiration. The user can revoke it at any time.

### Contracts (Base Sepolia)

| Contract | Address |
|----------|---------|
| **Calibur** | `0x000000009b1d0af20d8c6d0a44e162d11f9b8f00` |
| **USDC** | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| **Mock WETH** | `0xbabe0001489722187FbaF0689C47B2f5E97545C5` |

## API routes

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/airdrop` | POST | Sends 1 USDC to the specified address from the backend wallet |
| `/api/dca` | GET | Returns DCA config and verifies on-chain agent key status |
| `/api/dca` | POST | Enables or disables DCA — creates agent wallet on enable |
| `/api/dca/execute` | GET | Cron-triggered: executes DCA for all active agents via UserOps |

## Use cases

* **Dollar-Cost Averaging**: Automated recurring token purchases
* **Delegated Execution**: Agents acting on behalf of users with limited permissions
* **Time-Limited Access**: Session keys that automatically expire
* **Gasless UX**: Users interact without needing ETH for gas
* **Portfolio Automation**: Scheduled rebalancing or investment strategies
