Connect an agent

One proposed purchase. Three ways to ask.

An agent wants to spend €184.50 with Northstar Office Supply on office supplies, against cost centre OPS-2026-04. Below is that exact request through the MCP tool, over REST, and from the TypeScript SDK. The same Zod schema validates all three, the same engine decides them, and all three land in the same Activity record.

None of these can move money. A spend request is a question. The answer is recorded and returned; no supplier is contacted, no approver is asked and no rail is called. Execution does not exist in Shadow Mode.


1 — MCP: observe_spend_request

For an agent that speaks the Model Context Protocol. The server exposes six tools; this is the one that asks a policy question.

The MCP server acts for the agent configured by the operator through PAYGENTE_AGENT_EXTERNAL_ID. The tool caller cannot override that identity, and the tool takes no agent parameter.

Tool call
→ tool: observe_spend_request
  {
    "recipientId": "rcp_01JQ8F7Z9K3M4N5P6Q7R8S9T0V",
    "amount": "184.50",
    "category": "office",
    "commitmentType": "MERCHANT_ORDER",
    "purpose": "Desk chairs and monitor arms for the
                two new research hires.",
    "projectReference": "OPS-2026-04"
  }
Tool result
← result

  Your policy would have let this through without
  asking anyone.

  WITHIN_POLICY — within every limit the policy sets.

  Nothing was paid, ordered or reserved. This request
  was observed, not authorized.

  https://…/app/activity/spi_01JQ…

2 — REST: POST /api/v1/spend-intents

The decision comes back in the same response. An agent that had to poll for the answer would have acted before it arrived.

Request
POST /api/v1/spend-intents
Content-Type: application/json
X-Paygente-Demo: <token from POST /api/v1/demo/sessions>
Idempotency-Key: procurement-2026-04-02-0031

{
  "agentId": "agt_01JQ8F7Z9K3M4N5P6Q7R8S9T0V",
  "recipientId": "rcp_01JQ8F7Z9K3M4N5P6Q7R8S9T0V",
  "asset": "EUR",
  "amount": "184.50",
  "category": "office",
  "commitmentType": "MERCHANT_ORDER",
  "purpose": "Desk chairs and monitor arms for the two new research hires.",
  "projectReference": "OPS-2026-04"
}
Response
201 Created

{
  "intent": {
    "id": "spi_01JQ8F7Z9K3M4N5P6Q7R8S9T0V",
    "mode": "SHADOW",
    "money": {
      "asset": "EUR",
      "amountMinor": "18450",
      "amount": "184.50",
      "amountDisplay": "€184.50"
    },
    "category": "office",
    "commitmentType": "MERCHANT_ORDER",
    "projectReference": "OPS-2026-04"
  },
  "decision": {
    "outcome": "ALLOW",
    "explanation": "Your policy would have let this through
                    without asking anyone.",
    "reasons": [
      { "code": "WITHIN_POLICY",
        "message": "Within every limit the policy sets." }
    ],
    "engineVersion": "policy-engine@1.0.0"
  },
  "url": "https://…/app/activity/spi_01JQ…"
}

3 — TypeScript SDK

Provide a stable idempotency key for each logical request and reuse that same key when retrying it. Paygente replays the original result for the same key and payload, and rejects reuse with a different payload. The client forwards the key you give it and never invents one — a value minted per attempt would be new on every retry, which protects nothing.

submit-spend-request.ts
import { PaygenteClient } from '@paygente/sdk';

const paygente = new PaygenteClient({
  baseUrl: process.env.PAYGENTE_API_URL!,
  // Demo sessions only. Production authentication is not built yet.
  demoToken: process.env.PAYGENTE_DEMO_TOKEN!,
});

const { decision, url } = await paygente.submitSpendIntent(
  {
    agentId,
    recipientId,
    asset: 'EUR',
    amount: '184.50',
    category: 'office',
    commitmentType: 'MERCHANT_ORDER',
    purpose: 'Desk chairs and monitor arms for the two new research hires.',
    projectReference: 'OPS-2026-04',
  },
  // Stable across retries of this same order — not a fresh value each attempt.
  { idempotencyKey: 'procurement-2026-04-02-0031' },
);

if (decision.outcome === 'DENY') {
  // Do not buy it. Nothing was charged either way — Shadow Mode
  // records the answer, it does not act on it.
  console.error(decision.explanation, url);
}

Authentication, honestly

What works today

An anonymous demo session. POST /api/v1/demo/sessions returns a token; send it as X-Paygente-Demo. The token is shown once, it is scoped to one throwaway organization, and it expires.

What does not exist yet

API keys, service accounts, scopes and production authentication are not built. A demo session is not a production credential and must not be treated as one. Do not put real supplier data in the sandbox.