# Connect an agent to Orthogonal

Orthogonal lets agents discover APIs, inspect their schemas and prices, search the web, scrape pages, enrich people and companies, retrieve data, and call APIs using one account and balance. After connecting, read https://orthogonal.com/ai for the full workflow.

When asked to set up Orthogonal, **connect the account the user already has**. Do not start registration, collect their phone number, or ask them to copy an API key from the dashboard when they want to sign in.

## Choose the right account path

1. **Already connected:** reuse the host's existing Orthogonal connection. If an `ORTHOGONAL_API_KEY` is configured, validate it with `GET https://api.orthogonal.com/v1/credits/balance` and the bearer header. A successful response confirms API access. Never print the key. A failed request does not mean the user needs a new account.
2. **Existing account:** use the host's native MCP OAuth connection when available, or the browser connection below for an agent with HTTP access and secure credential storage. Give the user the sign-in link, wait for approval, save the returned key securely, verify access, and continue the original task.
3. **Account status unknown:** ask once whether the user already has an Orthogonal account. An email address or a browser session does not by itself authenticate the agent. The public guide cannot determine whether an email has an account.
4. **New account:** only use the email-and-phone registration steps below when the user wants a new agent account. Existing-account connection does not create an account or award starter credits.

## Connect an existing account in the browser

### Native MCP OAuth

If the host can add an OAuth MCP server, connect `https://mcp.orthogonal.com` using its native connection flow. Show the authorization link produced by the host, let the user sign in and approve access, then verify the connection by listing or calling its tools. Do not invent an OAuth URL or ask for a password or API key in chat.

An API-key-only custom connector cannot become an OAuth connector through these instructions. Use the direct API handoff below if the agent can securely store and use the returned credential. Otherwise, the host needs OAuth support; a browser sign-in alone will not authenticate that connector. Explain that limitation instead of claiming it is connected.

### Direct API handoff (local or remote agents)

This uses the same browser sign-in as the website. The agent keeps a private polling code, while the user opens a separate approval URL. No localhost listener, phone verification, or manual key copying is needed.

Start a connection (Bash, `curl`, and `jq`):

```sh
API_BASE="https://api.orthogonal.com"
CONNECTION=$(curl --fail-with-body --silent --show-error \
  --request POST "$API_BASE/api/agent-auth/start" \
  --header "Content-Type: application/json" \
  --data '{"agentName":"Research assistant"}')
DEVICE_CODE=$(printf '%s' "$CONNECTION" | jq -er '.deviceCode')
printf '%s\n' "$CONNECTION" | jq '{verificationUrl, userCode, expiresIn, pollExpiresIn, interval}'
```

Give the user only `verificationUrl` and `userCode`, with a short instruction: **Open this link, sign in to your existing account, confirm the matching code, and connect the agent. Then return here.** The agent name is a label, not a verified identity. Approval creates a dedicated personal API key that can call APIs, access account data, and spend available credits. The user can revoke it in API key settings.

Keep `deviceCode` private. Never put it in the browser URL, chat, shared logs, or a connector description. The start response reports `accountStatus: "unknown"`; it has not checked or authenticated any account.

While the user signs in, poll this endpoint no more frequently than every `interval` seconds (currently 5):

```sh
AUTH=$(curl --fail-with-body --silent --show-error \
  --request POST "$API_BASE/api/agent-auth/poll" \
  --header "Content-Type: application/json" \
  --data "$(jq -n --arg deviceCode "$DEVICE_CODE" '{deviceCode: $deviceCode}')")
printf '%s\n' "$AUTH" | jq '{status, accountStatus, accountId}'
```

| `status` | Next action |
| --- | --- |
| `pending` | Wait at least 5 seconds, then poll again. |
| `slow_down` | Wait at least the `Retry-After` header (5 seconds), then poll again. |
| `approved` | Read `apiKey` privately, save it in the host's secret store, and verify API access. `accountStatus` is now `existing`. |
| `denied` | Stop. The user declined, or the issued key was revoked or deleted. |
| `expired` or `invalid` | Stop. Ask whether to start a new connection; do not create an account. |

The user must approve within `expiresIn` seconds (10 minutes). Poll for at most `pollExpiresIn` seconds (11 minutes), stopping immediately on denial or expiry. The extra minute lets an approval near the deadline reach the agent. HTTP 429 includes `Retry-After`; honor it. On a network error or HTTP 503, back off and retry the same polling code while it is valid. Approval and credential delivery are idempotent until the original deadline or one minute after approval, whichever is later: a lost response does not require another approval or create another key. Deleted or revoked keys cannot be recovered.

Only after `status` is `approved`:

```sh
ORTHOGONAL_API_KEY=$(printf '%s' "$AUTH" | jq -er 'select(.status == "approved") | .apiKey')
export ORTHOGONAL_API_KEY
curl --fail-with-body --silent --show-error "$API_BASE/v1/credits/balance" \
  --header "Authorization: Bearer $ORTHOGONAL_API_KEY"
unset CONNECTION DEVICE_CODE AUTH
```

Save the key securely for future sessions; an exported shell variable lasts only for that process and its children. Do not print it or ask the user to paste it into chat. Report connection success only after the API check succeeds, then continue the requested task. If balance is insufficient, follow the credit steps below with the owner's approval.

## Create a new agent account

Create an agent account by verifying both your email address and phone number. A successful signup returns an Orthogonal API key; no password or existing API key is required.

Eligible new accounts receive **$1 in starter credits before the API key is returned**. The offer is one-time per account and phone number, shared with the website's free-credit offer. Repeating registration does not add another $1. A phone that already used the offer cannot receive another grant through a new account.

## Requirements

- A unique email address whose inbox you can access
- First and last name
- A phone number in E.164 format, such as `+14165550123`
- `curl` and `jq`

## 1. Request email and phone verification codes

```sh
API_BASE="https://api.orthogonal.com"
EMAIL="agent@example.com"
FIRST_NAME="Research"
LAST_NAME="Agent"
PHONE_NUMBER="+14165550123"

CHALLENGE=$(curl --fail-with-body --silent --show-error \
  --request POST "$API_BASE/api/agent-signup/send-code" \
  --header "Content-Type: application/json" \
  --data "$(jq -n \
    --arg email "$EMAIL" \
    --arg firstName "$FIRST_NAME" \
    --arg lastName "$LAST_NAME" \
    --arg phoneNumber "$PHONE_NUMBER" \
    '{email: $email, firstName: $firstName, lastName: $lastName, phoneNumber: $phoneNumber, acceptedTerms: true}')")

SIGNUP_ID=$(printf '%s' "$CHALLENGE" | jq -r '.signupId')
VERIFICATION_TOKEN=$(printf '%s' "$CHALLENGE" | jq -r '.verificationToken')
```

Orthogonal sends separate six-digit codes to the email address and phone number. Both expire after five minutes. You must own both identifiers. Wait at least 60 seconds before requesting replacement codes.

## 2. Verify both codes

```sh
read -r -s -p "Phone verification code: " CODE
read -r -s -p "Email verification code: " EMAIL_CODE

VERIFICATION=$(curl --fail-with-body --silent --show-error \
  --request POST "$API_BASE/api/agent-signup/verify-code" \
  --header "Content-Type: application/json" \
  --data "$(jq -n \
    --arg phoneNumber "$PHONE_NUMBER" \
    --arg code "$CODE" \
    --arg emailCode "$EMAIL_CODE" \
    --arg signupId "$SIGNUP_ID" \
    --arg verificationToken "$VERIFICATION_TOKEN" \
    '{phoneNumber: $phoneNumber, code: $code, emailCode: $emailCode, signupId: $signupId, verificationToken: $verificationToken}')")

SIGNUP_PROOF=$(printf '%s' "$VERIFICATION" | jq -r '.signupProof')
```

## 3. Create the account and receive its API key

```sh
ACCOUNT=$(curl --fail-with-body --silent --show-error \
  --request POST "$API_BASE/api/agent-signup/register" \
  --header "Content-Type: application/json" \
  --data "$(jq -n \
    --arg email "$EMAIL" \
    --arg firstName "$FIRST_NAME" \
    --arg lastName "$LAST_NAME" \
    --arg phoneNumber "$PHONE_NUMBER" \
    --arg signupProof "$SIGNUP_PROOF" \
    '{email: $email, firstName: $firstName, lastName: $lastName, phoneNumber: $phoneNumber, signupProof: $signupProof, acceptedTerms: true}')")

ORTHOGONAL_API_KEY=$(printf '%s' "$ACCOUNT" | jq -r '.apiKey')
export ORTHOGONAL_API_KEY
printf '%s\n' "$ACCOUNT" | jq '{accountId, starterCredit}'
```

Example response:

```json
{
  "accountId": "user_...",
  "apiKey": "orth_agent_...",
  "starterCredit": { "amountUsd": 1, "alreadyGranted": false }
}
```

Save the API key securely. Only its hash is stored in the API-key table; the dashboard cannot reveal it. Use it as `Authorization: Bearer <apiKey>` with the normal Orthogonal API.

If the account or phone is ineligible for the starter offer, registration still returns 201 with its API key, but `starterCredit` is `{ "amountUsd": 0, "alreadyGranted": false, "reason": "phone_unavailable" }` or uses `"reason": "ineligible"`. No starter credits were added. Save the key and follow step 4 to add credits with the account owner's approval; do not create another account to bypass the offer limit.

If the registration response is lost, retry `/register` with the same details and proof. The endpoint returns the same account and key, without creating duplicates. The proof expires after ten minutes. If it expires, request and verify new codes using the same email and phone number. Recovery is available for 24 hours after account creation and never restores a revoked or deleted key. After that, sign in to manage or replace your keys.

Do not change email or names between these steps. Verification is bound to the original details. A `429` response means a shared rate limit was reached; do not retry in a tight loop. A `503` during registration can mean creation is pending—retry the same request. A `409` means signup recovery is unavailable; use sign-in instead.

The examples use Bash and keep secrets in shell variables. Do not run with shell tracing (`set -x`), put codes or keys in URLs, or save responses in shared logs.

For the browser flow, visit [orthogonal.com/agent/signup](https://orthogonal.com/agent/signup).

## 4. Check balance and add credits

```sh
curl --fail-with-body --silent --show-error "$API_BASE/v1/credits/balance" \
  --header "Authorization: Bearer $ORTHOGONAL_API_KEY"
curl --fail-with-body --silent --show-error "$API_BASE/v1/credits/pricing"
```

Read the current packages and total prices. With the account owner's approval, request a Stripe link by email:

```sh
curl --fail-with-body --silent --show-error \
  --request POST "$API_BASE/v1/credits/email-payment-link" \
  --header "Authorization: Bearer $ORTHOGONAL_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"credits":25}'
```

This package adds $25 in credits; the current total is $26.25 including the 5% processing fee. Always use `/v1/credits/pricing` for current prices. The link goes to the account's verified primary email; custom recipients, account IDs and redirects are not accepted. Personal API keys only; organization purchases use the dashboard.

The response includes `emailSent`, `email`, `credits`, `totalLabel` and `checkoutUrl`. If email delivery fails (`emailSent: false`), use that checkout URL instead of creating more links. Limits are one request per minute and five per account per day. Honor 429 responses and do not retry in a loop.

Requesting a link does not charge anything or add credits. The owner pays through Stripe, and the existing payment webhook loads the credits after successful payment. Check balance again after payment confirmation. Do not complete payment without explicit authorization.

If the starter-credit service is temporarily unavailable, registration returns 503 rather than an unfunded key. Retry the same registration; account creation and the $1 grant are idempotent. `alreadyGranted: true` means the starter grant was previously applied, not that the current balance is still $1. Keep checkout URLs private.
