---
name: tangle-sandbox-setup
description: Set up Tangle Sandbox for this agent end to end: get a scoped API key with one owner approval, install the client, make the first successful call, and verify it.
---

# Set up Tangle Sandbox

Tangle Sandbox runs isolated Linux sandboxes for AI agents: a real filesystem, package installs, command execution, previews, long-running workspaces, and snapshot-backed templates. You drive it from the @tangle-network/sandbox SDK, the tangle CLI, or plain HTTPS.

Use it when:

- You need to run generated or untrusted code somewhere other than this machine.
- You need a clean Linux machine with Node.js, Python and git to install packages, build, or test a project.
- You need a long-running workspace, a preview URL for a server, or a snapshot you can resume later.

Complete every step below in order and stop at the first failure; the error table at the end names the fix.
Prerequisites: Node.js 18 or newer with npm; outbound HTTPS to id.tangle.tools, sandbox.tangle.tools and registry.npmjs.org; your owner's email address; one working directory for every step (for example `mkdir tangle-setup && cd tangle-setup`).

## 1. Get a scoped key (one owner approval)

Tangle issues each agent one key, owned by a person who approves it once. The same key works for every Tangle product the owner approves: Tangle Sandbox, Tangle Router and the agent apps such as Tangle GTM Agent. If you already hold one from another Tangle product's setup, reuse it and skip to step 2.
Ask your owner which email address their Tangle account uses (or should use). Nothing is charged by signing up, and there is no free credit: Tangle Sandbox work is paid from the owner's prepaid balance.

Save this script as `tangle-signup.mjs` (Node.js 18 or newer, no dependencies):

```js
// Requests a scoped Tangle key for this agent. The owner approves once.
//   node tangle-signup.mjs start --agent-name <name> [--owner-email <email>] [--budget-usd <n>] [--products a,b]
//   node tangle-signup.mjs wait    (run again while it exits 3; exits 0 once approved)
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"

const command = process.argv[2]
const flag = (name) => {
  const index = process.argv.indexOf(`--${name}`)
  return index > 0 ? process.argv[index + 1] : undefined
}
const platform = 'https://id.tangle.tools'
const statePath = '.tangle/signup.json'
mkdirSync('.tangle', { recursive: true, mode: 0o700 })

async function post(path, body) {
  const response = await fetch(platform + path, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  })
  return { status: response.status, body: await response.json().catch(() => ({})) }
}

if (command === 'start') {
  const request = { agent_name: flag('agent-name') ?? 'coding-agent' }
  if (flag('products')) request.products = flag('products').split(',').map((id) => id.trim()).filter(Boolean)
  if (flag('owner-email')) request.owner_email = flag('owner-email')
  if (flag('budget-usd')) request.budget_usd = Number(flag('budget-usd'))
  let started = await post('/cross-site/device/start', request)
  // Tangle caps the approval emails one owner receives. Past the cap the
  // owner approves from the link the agent hands them instead.
  let emailCapped = false
  if (started.status === 429 && started.body.error === 'owner_notification_throttled' && request.owner_email) {
    emailCapped = true
    delete request.owner_email
    started = await post('/cross-site/device/start', request)
  }
  if (started.status !== 200) {
    console.error(`Signup refused (HTTP ${started.status}): ${JSON.stringify(started.body)}`)
    process.exit(1)
  }
  const grant = started.body.data
  const saved = {
    device_code: grant.device_code,
    interval: grant.interval,
    expires_at: Date.now() + grant.expires_in * 1000,
  }
  writeFileSync(statePath, JSON.stringify(saved), { mode: 0o600 })
  if (emailCapped) {
    console.log('Tangle has already sent this owner the most approval emails it allows for now, so none was sent.')
  }
  console.log(grant.agent?.owner_notified
    ? `Approval email sent to ${request.owner_email}.`
    : 'Send your owner this approval link.')
  console.log(`Approval link: ${grant.verification_uri_complete}`)
  console.log(`Confirmation code: ${grant.user_code} (expires in ${Math.round(grant.expires_in / 60)} minutes)`)
  console.log('Next: run `node tangle-signup.mjs wait` until it prints Approved.')
  process.exit(0)
}

if (command === 'wait') {
  let saved
  try {
    saved = JSON.parse(readFileSync(statePath, 'utf8'))
  } catch {
    console.error('No pending signup. Run `node tangle-signup.mjs start` first.')
    process.exit(1)
  }
  const stopAt = Math.min(saved.expires_at, Date.now() + Number(flag('seconds') ?? 8) * 1000)
  for (;;) {
    const polled = await post('/cross-site/device/poll', { device_code: saved.device_code })
    if (polled.status === 200) {
      const { api_key: apiKey, key, account } = polled.body.data
      writeFileSync('.tangle/api-key', `${apiKey}\n`, { mode: 0o600 })
      console.log(`Approved. Key "${key?.name ?? 'agent'}" saved to .tangle/api-key for ${(key?.products ?? []).join(', ') || 'the approved products'} (spend cap $${key?.budget_usd ?? 'none'}).`)
      if (account && !account.funded) {
        console.log(`The owner's account has no credit yet. Ask the owner to add credits: ${account.add_credits_url}`)
      }
      process.exit(0)
    }
    if (polled.status !== 428) {
      console.error(`Approval failed (HTTP ${polled.status}): ${JSON.stringify(polled.body)}`)
      process.exit(1)
    }
    if (Date.now() >= saved.expires_at) {
      console.error('The approval link expired. Run `node tangle-signup.mjs start` again.')
      process.exit(1)
    }
    if (Date.now() + saved.interval * 1000 > stopAt) {
      console.log('Still waiting for the owner to approve. Run `node tangle-signup.mjs wait` again.')
      process.exit(3)
    }
    await new Promise((resolve) => setTimeout(resolve, saved.interval * 1000))
  }
}

console.error('Usage: node tangle-signup.mjs start --agent-name <name> [--owner-email <email>] [--budget-usd <n>] [--products a,b] | wait')
process.exit(2)
```

Start the request with your own name and the owner's email. It returns at once:

```bash
node tangle-signup.mjs start --agent-name "<your agent name>" --owner-email "<owner email>" --budget-usd 10
```

It prints the approval link and a confirmation code. Tangle emails the owner the same link; if you cannot give an email, omit `--owner-email` and send the owner the printed link yourself. When Tangle has already sent that owner the most approval emails it allows, `start` says so and prints the link without emailing it: send it to the owner yourself. Tell the owner the code if you can reach them.

Then wait for the approval. Each run returns within about 8 seconds: exit code 3 and `Still waiting` mean run it again; exit code 0 and `Approved` mean the key is saved. Keep each run short rather than wrapping it in one long blocking loop, since many agent tools stop long commands.

```bash
node tangle-signup.mjs wait
```

What the owner does, once: sign in (or create an account and verify the email), check the confirmation code, and approve. That is the only human step.

The key in `.tangle/api-key` (mode 600) works only for the products the owner approved, can spend at most the cap you requested across all of them, and the owner can revoke it once under Keys to stop it everywhere. If `wait` says the account has no credit, ask the owner to add credits at the printed link before your first paid call.

Then load the key without printing it, and keep it out of version control:

```bash
export TANGLE_API_KEY="$(cat .tangle/api-key)"
grep -qxF '.tangle/' .gitignore 2>/dev/null || echo '.tangle/' >> .gitignore
```

Never print, log, paste, or commit the key.

## 2. Install

Install the SDK in the working directory:

```bash
npm init -y >/dev/null
npm install @tangle-network/sandbox
```

Expected: npm prints `added N packages` and exits 0.

## 3. Make the first call

Create a sandbox, run a command in it, and delete it:

```bash
cat > first-sandbox.mjs <<'EOF'
import { Sandbox } from '@tangle-network/sandbox'

const client = new Sandbox({ apiKey: process.env.TANGLE_API_KEY, baseUrl: 'https://sandbox.tangle.tools' })
const box = await client.create({ name: 'agent-setup-first-call' })
try {
  await box.waitFor('running')
  const result = await box.exec('echo hello from tangle && uname -s')
  if (result.exitCode !== 0) throw new Error(result.stderr)
  console.log(result.stdout.trim())
  console.log(`sandbox ${box.id} ran the command`)
} finally {
  await box.delete()
  console.log('sandbox deleted')
}
EOF
node first-sandbox.mjs
```

Expected: four lines: `hello from tangle`, `Linux`, `sandbox sandbox-<id> ran the command`, `sandbox deleted`. It takes about 10 seconds.

## 4. Verify it worked

Confirm the key identifies the owner's account:

```bash
curl -fsS -H "Authorization: Bearer $TANGLE_API_KEY" https://sandbox.tangle.tools/v1/me | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=JSON.parse(s).data;console.log('key', d.api_key_id, 'for', d.email)})"
```

Expected: `key key_<id> for <owner email>`: the account your owner approved.

## Common errors

| Symptom | Cause | Fix |
| --- | --- | --- |
| Signup refused (HTTP 429) `owner_notification_throttled` | Too many approval emails for this owner or from this machine. | Wait the `retry_after` seconds, or omit `--owner-email` and send the owner the printed link yourself. |
| Signup refused (HTTP 400) `invalid_request` | The owner email or agent name is malformed, or the address cannot receive mail. | Use the owner's real inbox; the agent name may use letters, digits, spaces and . _ ( ) -. |
| `The approval link expired. Run node tangle-signup.mjs start again.` | The owner did not approve within 15 minutes. | Run `node tangle-signup.mjs start` again; a new code is issued. |
| Owner sees "Different account" on the approval page | The owner is signed in to a Tangle account other than the email you named. | Ask the owner to switch to that account, or rerun signup with the email they are signed in as. |
| HTTP 403 `paid_access_required` when creating a sandbox | The owner's account has no credit. Sandbox has no free tier or free credit. | Ask the owner to add credits at https://id.tangle.tools/app/billing, then rerun the first call. |
| HTTP 401 `invalid_platform_key` or `unauthorized` | TANGLE_API_KEY is unset, mistyped, or the owner revoked the key. | Run `export TANGLE_API_KEY="$(cat .tangle/api-key)"`; if it still fails, rerun signup. |
| HTTP 402 or a budget error after some use | The key reached the spend cap the owner approved. | Ask the owner to raise the key's cap under Keys, or rerun signup with a larger --budget-usd. |
| `Cannot find package '@tangle-network/sandbox'` | The script ran outside the directory where the SDK was installed. | Run every step from the same working directory. |

## Pricing

A running sandbox accrues per second from the owner's prepaid credit at an hourly rate of vCPU x cpuCoreHourUsd + memory GB x memoryGBHourUsd + disk GB x diskGBHourUsd, never below minChargeHourUsd. Outbound traffic beyond the plan's allowance is billed per GB. There is no free credit: paid calls need prepaid credit on the owner's account.

Current prices, no key needed: https://sandbox.tangle.tools/v1/pricing/sandboxes
Pricing page: https://id.tangle.tools/pricing

## Next

- [Quickstart](https://sandbox.tangle.tools/docs/quickstart)
- [TypeScript SDK reference](https://sandbox.tangle.tools/docs/sdk/typescript)
- [Python SDK](https://sandbox.tangle.tools/docs/sdk/python)
- [tangle CLI](https://sandbox.tangle.tools/docs/cli)
- [Workflows: durable graph workflows on the same platform](https://sandbox.tangle.tools/docs/workflows)
- [Public templates (no key needed)](https://sandbox.tangle.tools/v1/public-templates)

## Machine-readable surfaces

- Agent manifest: https://sandbox.tangle.tools/.well-known/tangle-agent.json
- llms.txt: https://sandbox.tangle.tools/llms.txt
- This setup skill: https://sandbox.tangle.tools/agent-setup.md
- MCP server: https://id.tangle.tools/mcp (Bearer TANGLE_API_KEY; the key needs sandbox:* control-plane scopes)
