---
name: tangle-router-setup
description: Set up Tangle Router 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 Router

Tangle Router is an OpenAI-compatible API across providers and operators, with model discovery, usage attribution, and per-request price headers. Browse models at /models; with a key, read current availability from /v1/models. Point an OpenAI client at https://router.tangle.tools/v1 with a Tangle key.

Use it when:

- You need a language, embedding, image or speech model behind one OpenAI-compatible endpoint.
- You want to switch models or providers without changing client code.
- You need each response to report the model that served it and what it cost.

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 (for the signup script) and curl; outbound HTTPS to id.tangle.tools and router.tangle.tools; 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 Router 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 5
```

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

No install is needed for HTTP; to use the SDK and CLI instead, install it:

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

Expected: npm prints `added N packages` and exits 0. The unscoped `tcloud` package on npm is unrelated; use the scoped name.

## 3. Make the first call

Send one chat completion:

```bash
curl -sS https://router.tangle.tools/v1/chat/completions \
  -H "Authorization: Bearer $TANGLE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"model":"gpt-4o-mini","max_tokens":10,"messages":[{"role":"user","content":"Reply with exactly: hello from tangle"}]}' \
  | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const r=JSON.parse(s);if(!r.choices)throw new Error(s);console.log(r.model, '->', r.choices[0].message.content)})"
```

Expected: The response prints the served model ID and its reply. Read current prices from /v1/models and the response price/usage metadata; do not assume a fixed cost or model snapshot.

## 4. Verify it worked

Confirm the served model and the charge:

```bash
curl -sS -o /dev/null -D - https://router.tangle.tools/v1/chat/completions \
  -H "Authorization: Bearer $TANGLE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"model":"gpt-4o-mini","max_tokens":5,"messages":[{"role":"user","content":"ok"}]}' \
  | grep -i -E '^(HTTP|x-tangle-served-model|x-tangle-price-)'
```

Expected: A successful request reports HTTP 200, x-tangle-served-model, x-tangle-price-input and x-tangle-price-output. The served identity can be a canonical or snapshot ID; compare it with the requested model and fallback policy.

## 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 . _ ( ) -. |
| Approval page says the account needs credits | Router keys require a funded account before approval; there is no free credit. | Ask the owner to add credits at https://id.tangle.tools/app/billing, then approve again or rerun signup. |
| `The approval link expired. Run node tangle-signup.mjs start again.` | The owner did not approve before the approval code expired. | Run `node tangle-signup.mjs start` again; a new code is issued. |
| HTTP 402 with `X-Payment-Required` | The request had no valid key, or the account or key has no spendable balance left. | Export TANGLE_API_KEY from .tangle/api-key; if it is set, ask the owner to add credits or raise the key's cap under Keys. |
| HTTP 401 `invalid_key` | The key is mistyped or the owner revoked it. | Run `export TANGLE_API_KEY="$(cat .tangle/api-key)"`; if it still fails, rerun signup. |
| HTTP 404 or `model_not_found` | The model id is not in the catalog. | List ids with `curl -sS https://router.tangle.tools/v1/models -H "Authorization: Bearer $TANGLE_API_KEY"` and use one exactly. |

## Pricing

Each request is charged per input and output token at the model's listed price from the owner's prepaid credit; every response reports the price in `x-tangle-price-input` and `x-tangle-price-output`. There is no free credit.

Current prices, no key needed: https://router.tangle.tools/models
Pricing page: https://router.tangle.tools/pricing

## Next

- [API docs](https://router.tangle.tools/docs)
- [Model catalog (no key needed)](https://router.tangle.tools/models)
- [Tangle Sandbox setup skill](https://sandbox.tangle.tools/agent-setup.md)
- [Crypto-native payment lane (agents holding keys)](https://router.tangle.tools/for-agents-crypto.md)
- [Run an inference operator (agents operating a host)](https://router.tangle.tools/for-operators.md)

## Machine-readable surfaces

- Agent manifest: https://router.tangle.tools/.well-known/tangle-agent.json
- llms.txt: https://router.tangle.tools/llms.txt
- This setup skill: https://router.tangle.tools/agent-setup.md
- OpenAPI: https://router.tangle.tools/openapi.json
