Skip to main content

Documentation Index

Fetch the complete documentation index at: https://glide-9da73dea.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

This recipe walks the full agent payment lifecycle end-to-end: define a policy envelope that caps spend per transaction and allowlists a vendor address, get a scoped grant from the OAuth authorization server, call payments.initiate, then query audit.stream to confirm the event landed. This is the canonical path every production agent should follow. Audience: agent authors who want to go beyond quickstart toy calls.

Prerequisites

  • The axtior-neobank repo cloned locally with pnpm install run at the root.
  • apps/mcp running on localhost:3001 (default port from apps/mcp/src/server.ts).
  • Node 22+ and pnpm.
  • A Glide OAuth client with payments:initiate and audit:stream scopes.

Steps

1. Clone the example

cd axtior-neobank/examples/agent-pays-vendor
pnpm install

2. Set environment variables

cp .env.example .env
# .env
GLIDE_BASE_URL=http://localhost:3001
GLIDE_GRANT_TOKEN=         # JWT from OAuth AS — see step 4
GLIDE_VAULT_ID=            # UUID of the vault to gate
GLIDE_ENTITY_ID=           # UUID of the entity the vault belongs to

3. Define the policy envelope (src/0-policy.ts)

AgentPolicyEnvelope is the 14-axis object evaluated by @glideco/policy-engine on every agent tool call. All amount caps are integer cents (not dollar strings).
// src/0-policy.ts
import { agentPolicyEnvelopeSchema } from '@glideco/schemas';

const policyInput = {
  policy_id: '01927fff-0000-7000-8000-000000000001',
  vault_id: process.env['GLIDE_VAULT_ID']!,
  policy_version: 1,

  // Amount caps — all integers, USD cents
  amount_cap_cents_per_tx: 50_000,    // $500
  amount_cap_cents_per_day: 200_000,  // $2,000
  step_up_amount_cents: 20_000,       // step-up required above $200

  // Counterparty allowlist — (address, chain, token) triple required.
  // Address-only is unsafe: the same hex string has different meaning
  // on different chains (F1 sanctions-cache-chain-key rule).
  counterparty_allowlist: [
    {
      address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
      chain: 'base',
      token: 'USDC',
    },
  ],

  chain_allowlist: ['base', 'ethereum'],
  velocity_max_txs_per_hour: 10,

  created_at: new Date().toISOString(),
  updated_at: new Date().toISOString(),
};

const result = agentPolicyEnvelopeSchema.safeParse(policyInput);
if (!result.success) {
  for (const issue of result.error.issues) {
    console.error(`  ${issue.path.join('.')}: ${issue.message}`);
  }
  process.exit(1);
}

export const policy = result.data;
Key fields:
  • policy_id — stable UUID across version bumps; (vault_id, policy_id) is unique.
  • policy_version — monotonic counter; cache invalidation key for the policy engine.
  • amount_cap_cents_* — absent means no cap on that axis; the engine still evaluates other gates.
  • counterparty_allowlist — non-empty array closes the gate (only listed counterparties allowed); empty array (or omitted) means no counterparty restriction. Match the source semantics in policy-engine/src/evaluate.ts.

4. Issue a scoped grant (src/1-grant.ts)

In production, your agent runtime calls the OAuth AS with client_credentials to get a JWT. The mock below shows the shape the AS issues.
// src/1-grant.ts
import { grantClaimsSchema } from '@glideco/schemas';

const now = Math.floor(Date.now() / 1000);

// In production — exchange client credentials for a real JWT:
//
//   curl -s -X POST https://ory.glide.co/oauth2/token \
//     -u "$GLIDE_OAUTH_CLIENT_ID:$GLIDE_OAUTH_CLIENT_SECRET" \
//     -d grant_type=client_credentials \
//     -d "scope=payments:initiate audit:stream" \
//     | jq -r .access_token

const mockClaims = {
  iss: 'https://ory.glide.co',
  sub: 'user-demo-00000000-0000-0000-0000-000000000001',
  act: { sub: 'agent-demo-00000000-0000-0000-0000-000000000001' },
  azp: 'claude-desktop',
  aud: {
    vault_id: process.env['GLIDE_VAULT_ID']!,
    entity_id: process.env['GLIDE_ENTITY_ID']!,
  },
  scope: ['payments:initiate', 'audit:stream'] as const,
  policy_version: 1,
  iat: now,
  nbf: now,
  exp: now + 3600,  // 60-minute cap — MAX_GRANT_TTL_SECONDS
  jti: '01927fff-0000-7000-8000-000000000002',
};

const result = grantClaimsSchema.safeParse(mockClaims);
if (!result.success) {
  for (const issue of result.error.issues) {
    console.error(`  ${issue.path.join('.')}: ${issue.message}`);
  }
  process.exit(1);
}

export const mockGrantClaims = result.data;
Grant fields:
  • sub + act.sub — human principal and the agent acting on their behalf (RFC 8693 actor claim).
  • aud.vault_id + aud.entity_id — both required; the gateway checks tenant isolation on every call.
  • policy_version — pinned at issuance; the verifier re-checks against the live policy on each call.
  • Max TTL is 3600 seconds (enforced by grantClaimsValidatedSchema).

5. Call payments.initiate and inspect the Receipt (src/2-pay.ts)

The MCP gateway runs on three category-scoped endpoints. Write tools live on /mcp/write.
// src/2-pay.ts
import { receiptSchema } from '@glideco/schemas';

const token = process.env['GLIDE_GRANT_TOKEN']!;
const baseUrl = process.env['GLIDE_BASE_URL'] ?? 'https://api.glide.co';

const res = await fetch(`${baseUrl}/mcp/write`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: crypto.randomUUID(),
    method: 'tools/call',
    params: {
      name: 'payments.initiate',
      arguments: {
        counterparty: {
          address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
          chain: 'base',
          token: 'USDC',
        },
        amount_cents: 5_000,    // $50 — below step_up_amount_cents ($200)
        currency: 'USDC',
        idempotency_key: 'vendor-pay-invoice-042',
        memo: 'invoice 042',
      },
    },
  }),
});

const body = await res.json();
// payments.initiate returns kind: 'accepted' | 'pending_step_up' (see paymentsInitiateOutput).
// On 'accepted', the gateway has enqueued the payment; the Receipt is written
// to activity_log when the connector settles. Poll audit.stream to observe it.
const result = body.result;
if (result.kind === 'pending_step_up') {
  console.log('step_up_url:', result.step_up_url);
  // Open the URL in the user's browser; retry the call with the same
  // idempotency_key after the user completes the Privy biometric step-up.
  process.exit(0);
}
console.log('pending_payment_id:', result.pending_payment_id);
console.log('policy_version:', result.policy_version);
console.log('risk_verdict:', result.risk_verdict);
console.log('enqueued_at:', result.enqueued_at);
Output fields (from paymentsInitiateOutput):
  • kindaccepted (enqueued) or pending_step_up (caller must complete biometric step-up first).
  • pending_payment_id — UUID of the row in agent_pending_payments. Correlate with audit.stream events as they arrive.
  • policy_version — pinned at evaluate time; the live policy may have advanced by settle time.
  • risk_verdictallow or allow_with_step_up. Deny verdicts come back as JSON-RPC errors, not as outputs.
  • enqueued_at — when the gateway accepted the call.
Once the connector settles, a Receipt row lands in activity_log and an AgentActivityEvent is emitted to the SSE audit stream. The fields you’ll read off the receipt:
  • receipt_id — primary key; use this to correlate against audit.stream events.
  • on_chain_tx — server-fetched from chain RPC, never from a facilitator response body (F1).
  • risk_verdictallow | allow_with_step_up. Denied calls do not produce receipts.
  • vendor_used — connector slug that settled the tx (matches ConnectorManifest.slug).
If payments.initiate returns JSON-RPC error code -32003 with step_up_url, open that URL in the user’s browser, wait for Privy WebAuthn to complete, then retry the call with the same idempotency_key.

6. Subscribe to the audit stream (src/3-audit.ts)

Read tools live on /mcp/read. audit.stream returns an sse_url and cursor — connect to the URL with EventSource (or any SSE client) to receive AgentActivityEvent rows as they’re written.
// src/3-audit.ts
const res = await fetch(`${baseUrl}/mcp/read`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: crypto.randomUUID(),
    method: 'tools/call',
    params: {
      name: 'audit.stream',
      arguments: {
        // Optional. Resume from this ISO-8601 instant; omit to start from
        // the moment the cursor was minted.
        from_iso: new Date(Date.now() - 60_000).toISOString(),
      },
    },
  }),
});

const { sse_url, cursor, expires_at, heartbeat_seconds } = (await res.json()).result;
console.log('sse_url:', sse_url);
console.log('cursor expires:', expires_at);

// Subscribe via standard EventSource — the SSE endpoint streams
// AgentActivityEvent rows scoped to this agent + grant.
const events = new EventSource(sse_url);
events.addEventListener('message', (raw) => {
  const event = JSON.parse(raw.data);
  // Filter for the payment we just made.
  if (event.event_kind !== 'tool_call_settled') return;
  if (event.pending_payment_id !== result.pending_payment_id) return;
  console.log('settled:', event.receipt_id, event.tool_call, event.risk_verdict);
  events.close();
});

Run it

GLIDE_VAULT_ID=<your-vault-id> \
GLIDE_ENTITY_ID=<your-entity-id> \
GLIDE_GRANT_TOKEN=<oauth-jwt> \
npx tsx src/0-policy.ts
npx tsx src/2-pay.ts
npx tsx src/3-audit.ts
Expected output:
[0] policy envelope valid.
    policy_id=01927fff-0000-7000-8000-000000000001
    amount_cap_cents_per_tx=50000 ($500)
    step_up_amount_cents=20000 ($200)
[2] payment initiated.
    receipt_id=01927fff-0000-7000-8000-000000000003
    rail=usdc-base  amount=$50  risk_verdict=allow
[3] audit event found.
    tool_call=payments.initiate  risk_verdict=allow
sequence complete.

Extend it

  • Set amount_cents above step_up_amount_cents to trigger the step-up path — payments.initiate returns kind: 'pending_step_up' with a step_up_url to open in the browser.
  • Add a second counterparty address NOT in counterparty_allowlist and confirm the policy engine rejects it.
  • Reduce velocity_max_txs_per_hour to 1 and submit two calls in the same minute to hit the velocity gate.
  • Bump policy_version without updating amount_cap_cents_per_tx — the grant’s pinned policy_version will no longer match and calls will fail with PolicyStaleError.

Source

github.com/darshanbathija/axtior-neobank/tree/main/examples/agent-pays-vendor

Reading list