Skip to content
DexterDocs
Dexter Connect / Bring Your Own Agent
Sections

Bring Your Own Agent

Connect your own agent to a Dexter account. The owner reviews its request in Dexter Wallet and chooses the permissions it receives. The agent gets a token for that connection and can inspect its approved spending limits with @dexterai/connect.

This guide uses a Node process running on the same computer as the owner's browser. It completes OAuth with a local callback, then prints the grant's status. For an existing client such as Claude or Cursor, use OpenDexter setup. See the Connect overview for account sign-in and owner wallet integrations.

Install

Use Node.js 20 or newer and an existing Dexter Wallet.

npm install @dexterai/connect@0.29.1 @dexterai/vault@0.43.4

Complete the Connection

Save this as connect-agent.mjs. It registers a public OAuth client, opens a local callback listener, and prints the authorization URL. PKCE binds the returned code to this process; the callback also checks the request's state.

connect-agent.mjs
import { createHash, randomBytes } from 'node:crypto';
import { createServer } from 'node:http';
import { readAgentAuthority } from '@dexterai/connect';

const api = 'https://api.dexter.cash';
const resource = 'https://open.dexter.cash/mcp';
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
const state = randomBytes(32).toString('base64url');

async function post(path, body) {
  const response = await fetch(`${api}${path}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(15_000),
  });
  const result = await response.json();
  if (!response.ok) {
    throw new Error(`${path}: ${result.error ?? response.status}`);
  }
  return result;
}

let settleCallback;
const callback = new Promise((resolve) => { settleCallback = resolve; });
let callbackUsed = false;
const server = createServer((request, response) => {
  const url = new URL(request.url ?? '/', 'http://127.0.0.1');
  if (request.method !== 'GET' || url.pathname !== '/callback') {
    response.writeHead(404).end();
    return;
  }
  if (url.searchParams.get('state') !== state || callbackUsed) {
    response.writeHead(400).end('Invalid or already used connection callback.');
    return;
  }
  const code = url.searchParams.get('code');
  const error = url.searchParams.get('error');
  if (!code && !error) {
    response.writeHead(400).end('Missing authorization code.');
    return;
  }
  callbackUsed = true;
  settleCallback(error ? { error } : { code });
  response.writeHead(200, {
    'content-type': 'text/plain; charset=utf-8',
    'cache-control': 'no-store',
  }).end(error ? 'Connection declined. Return to your terminal.'
    : 'Approval received. Return to your terminal.');
});

await new Promise((resolve, reject) => {
  server.once('error', reject);
  server.listen(0, '127.0.0.1', resolve);
});
const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
let expiryTimer;

try {
  const client = await post('/api/mcp/dcr/register', {
    client_name: 'Local Agent Example',
    redirect_uris: [redirectUri],
    grant_types: ['authorization_code', 'refresh_token'],
    response_types: ['code'],
    token_endpoint_auth_method: 'none',
    application_type: 'native',
    pkce_required: true,
  });
  if (typeof client.client_id !== 'string') throw new Error('Missing client ID.');

  const authorize = new URL('/api/connector/oauth/authorize', api);
  authorize.search = new URLSearchParams({
    client_id: client.client_id,
    redirect_uri: redirectUri,
    response_type: 'code',
    scope: 'vault',
    resource,
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  }).toString();
  console.log('Open this URL in your browser:');
  console.log(authorize.href);

  const result = await Promise.race([
    callback,
    new Promise((resolve) => {
      expiryTimer = setTimeout(() => resolve({ error: 'approval_timeout' }), 600_000);
    }),
  ]);
  if (result.error) throw new Error(result.error);

  const tokens = await post('/api/connector/oauth/token', {
    grant_type: 'authorization_code',
    client_id: client.client_id,
    redirect_uri: redirectUri,
    code: result.code,
    code_verifier: verifier,
    resource,
  });
  if (typeof tokens.access_token !== 'string') throw new Error('Missing access token.');

  const grant = await readAgentAuthority({ accessToken: tokens.access_token });
  console.log(JSON.stringify({
    active: grant.active,
    mode: grant.mode,
    inactiveReason: grant.inactiveReason,
    expiresAt: grant.expiresAt,
    remaining: grant.capacity,
  }, null, 2));
} finally {
  clearTimeout(expiryTimer);
  server.close();
}

Run it:

node connect-agent.mjs

Open the printed URL on the same computer. Dexter displays the requesting client and asks the owner to select their wallet. Review the permissions, set the spending limits you want, and approve. The browser returns to the local callback; your terminal prints the grant status.

Use the Approved Grant

A connection with active payment authority reports active: true and mode: "bounded_payment_authority". remaining contains the per-call, daily, and aggregate limits and usage. USDC amounts have six decimal places: 1000000 represents 1 USDC.

Keep the returned tokens in your agent's credential store and send its access token as a Bearer token when connecting to https://open.dexter.cash/mcp. The example keeps them in memory and exits after reading the grant. Keep tokens out of model prompts, logs, and checked-in files.

Your agent can then use OpenDexter's tools. The service checks each requested action against the owner's grant. A completed sign-in can also have active: false; use the returned mode and inactiveReason to decide whether another owner approval is needed before a paid action.

When the access token expires, refresh through the same token endpoint using grant_type: 'refresh_token', the returned refresh_token, and the same client_id. Replace the stored access token with the response. A refused or revoked refresh requires a new owner connection.

If Connection Stops

Keep the script running while approving; its callback belongs to that process. An expired request needs a new run. If the browser is on another computer, the local callback cannot reach the waiting agent. Use an OAuth client integration with an approved hosted callback for that deployment.

The owner can manage the connected agent and its permissions. Closing this example process ends its local session; removing the agent's authority is a separate owner action.

Last Updated:

On this page