# Build a Coding Agent Using Letta Code and Daytona (/docs/guides/letta-code/letta-code-agent)

This guide demonstrates how to run an autonomous coding agent based on [Letta Code](https://docs.letta.com/letta-code/) inside a Daytona sandbox environment. The agent can develop web apps, write code in any language, install dependencies, and run scripts. Letta Code uses stateful agents with built-in memory, allowing conversations to persist across sessions.

---

### 1. Workflow Overview [#1-workflow-overview]

When you launch the main script, a Daytona sandbox is created and Letta Code is installed inside it. The agent is configured with a custom Daytona-aware system prompt.

The script provides an interactive CLI interface where you can chat with the agent and issue commands:

```
$ npm run start
Creating sandbox...
Installing Letta Code...
Starting Letta Code...
Initializing agent...
Agent initialized. Press Ctrl+C at any time to exit.

User: create a beautiful, professional themed app that lets me write markdown documents and render them live
Thinking...

🔧 TodoWrite
🔧 Write /home/daytona/markdown-editor/index.html
🔧 TodoWrite
🔧 Start HTTP server on port 8080
🔧 TodoWrite
Perfect! I've created a beautiful markdown editor with live preview for you! 🎉

## Access your app here:
https://8080-c157e5cb-5e11-4bb6-883d-c873169223b8.proxy.daytona.works

## Features:

✨ **Live Preview** — Real-time markdown rendering  

📝 **Full Markdown Support** — Headers, text styles, lists, code blocks, tables, links, images  

💾 **Auto-Save** — Persists to browser localStorage  

📥 **Export** — Download as `.md` or standalone `.html`
```

The agent can host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/preview/) feature. When your task involves running or previewing a web application, the agent automatically hosts the app and generates a link for you to inspect the live result:

<Image
  src="/docs/images/letta-code-agent-result.gif"
  alt="Markdown editor demo generated by Letta Code agent"
  width={600}
/>

You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.

### 2. Project Setup [#2-project-setup]

#### Clone the Repository [#clone-the-repository]

First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:

```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/letta-code
```

#### Configure Environment [#configure-environment]

Get your Daytona API key from the [Daytona Dashboard](https://app.daytona.com/dashboard/keys) and your Letta API key from [Letta Platform](https://app.letta.com/api-keys).

Copy `.env.example` to `.env` and add your keys:

```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_LETTA_API_KEY=your_letta_api_key
```

<Callout type="caution" title="API Key Security">
In this example, your Letta API key is passed into the sandbox environment and may be accessible to any code executed within it.
</Callout>

#### Alternative: Inject the Key as a Daytona Secret [#alternative-inject-the-key-as-a-daytona-secret]

The default setup passes the Letta key into the sandbox as a plain environment variable, so anything running inside the sandbox - including the agent itself - can read the raw key with `env`. [Daytona Secrets](/docs/secrets) keep the raw value out of the sandbox entirely: the environment variable holds only an opaque placeholder (`dtn_secret_<id>`), and Daytona's outbound proxy substitutes the real value into HTTPS request headers at egress - and only for requests to the hosts the Secret allows. An agent that dumps the environment, or sends it to a host outside the allowlist, exposes only the placeholder. The proxy does not rewrite responses, so an allowed host that echoes the key returns the real value into the sandbox.

The Secret-based flow needs `@daytona/sdk` 0.192.0 or newer and a one-time Secret setup:

1. Create the Secret once for your organization - in the [Daytona Dashboard](https://app.daytona.com/dashboard/secrets) or with a one-off script (save as `create-secret.ts` next to this guide's `.env` and run `npx tsx create-secret.ts`):

   ```typescript
   import { Daytona } from '@daytona/sdk'
   import * as dotenv from 'dotenv'

   dotenv.config()

   async function main() {
     const value = process.env.SANDBOX_LETTA_API_KEY
     if (!value) throw new Error('SANDBOX_LETTA_API_KEY is not set')

     const daytona = new Daytona()
     await daytona.secret.create({
       name: 'letta-api-key',
       value,
       hosts: ['api.letta.com'], // the only host the real key may be sent to
     })
   }

   main()
   ```

2. In `src/index.ts`, swap the `LETTA_API_KEY` env var for a `secrets:` mapping (environment variable name to Secret name):

   ```diff
    sandbox = await daytona.create({
   -  envVars: { LETTA_API_KEY: process.env.SANDBOX_LETTA_API_KEY },
   +  secrets: { LETTA_API_KEY: 'letta-api-key' },
    })
   ```

Inside the sandbox, `env` now shows `LETTA_API_KEY=dtn_secret_...`, yet Letta Code still authenticates: it sends the key as an HTTPS `Authorization` header to `api.letta.com`, where the proxy swaps in the real value. Substitution happens only in HTTPS requests to allowed hosts - requests to any other host carry the harmless placeholder. See the [Secrets documentation](/docs/secrets#substitution-scope) for the full substitution scope.

#### Local Usage [#local-usage]

<Callout type="note" title="Node.js Version">
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
</Callout>

Install dependencies:

```bash
npm install
```

Run the example:

```bash
npm run start
```

The Letta Code agent will initialize and present an interactive prompt where you can issue commands.

### 3. Understanding the Agent's Architecture [#3-understanding-the-agents-architecture]

This example consists of two main TypeScript files:

- **index.ts**: The main program that creates the Daytona sandbox, installs Letta Code, configures the system prompt, and provides an interactive CLI interface.
- **letta-session.ts**: A helper class that manages PTY-based bidirectional communication with Letta Code, handling JSON message streaming and response parsing.

#### Initialization [#initialization]

On initialization, the main program:
1. Creates a new [Daytona sandbox](/docs/sandboxes) with your Letta API key included in the environment variables.
2. Installs Letta Code globally inside the sandbox by running `npm install` with [process execution](/docs/process-code-execution#command-execution).
3. Creates a [PTY (pseudoterminal)](/docs/pty/) session in the sandbox for bidirectional communication with Letta Code.
4. Launches Letta Code in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/) with stream-json format through the PTY.
5. Waits for user input and sends prompts to the agent through the PTY session.

#### Main Program Code [#main-program-code]

The program creates a [Daytona sandbox](/docs/sandboxes) with the Letta API key passed as an environment variable:

```typescript
sandbox = await daytona.create({
  envVars: { LETTA_API_KEY: process.env.SANDBOX_LETTA_API_KEY },
})
```

#### Running Letta Code in a Pseudoterminal [#running-letta-code-in-a-pseudoterminal]

A [PTY (pseudoterminal)](/docs/pty/) is created for bidirectional communication with Letta Code:

```typescript
this.ptyHandle = await this.sandbox.process.createPty({
  id: `letta-pty-${Date.now()}`,
  onData: (data: Uint8Array) => this.handleData(data),
})
```

Letta Code is then launched in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/#bidirectional-mode) through the PTY:

```typescript
await this.ptyHandle.sendInput(
  `letta --new --system-custom "${systemPrompt.replace(/"/g, '\\"')}" --input-format stream-json --output-format stream-json --yolo -p\n`,
)
```

The `stream-json` setting is used for the input and output formats, enabling our program to send and receive JSON messages to and from the agent in real-time.

The `--system-custom` prompt allows us to pass a custom system prompt to the agent. Our prompt configures the agent with Daytona-specific instructions, including a URL pattern so the agent can generate preview links.

The `--yolo` flag allows the agent to run shell commands without requiring explicit user approval for each command.

#### Message Handling [#message-handling]

To send prompts to the agent, the main script calls the `processPrompt()` method, which formats the user input as a JSON message and sends it to the PTY using `this.ptyHandle.sendInput()` as demonstrated above.

Formatted user messages look like this:

```json
{"type": "user", "message": {"role": "user", "content": "create a simple web server"}}
```

The agent responds with streaming JSON messages. Tool calls arrive as fragments:

```json
{"type": "message", "message_type": "approval_request_message", "tool_call": {"tool_call_id": "call_123", "name": "Bash", "arguments": "{\"command\": \"python3 -m http.server 8080\"}"}}
```

These JSON fragments are parsed by the `handleParsedMessage()` method. When multiple consecutive fragments are received for the same tool call, they are combined into a single tool call object. When a tool call or message is finished, the result is formatted and displayed to the user.

#### Clean up [#clean-up]

When you exit the main program, the Daytona sandbox and all files are automatically deleted.

**Key advantages:**

- Secure, isolated execution in Daytona sandboxes
- Stateful agents with persistent memory across sessions
- Full Letta Code capabilities including file operations and shell commands
- Agents can be viewed in [Letta's Agent Development Environment](https://app.letta.com/)
- Automatic preview link generation for hosted services
- Multi-language and full-stack support
- Simple setup and automatic cleanup
