Connect an agent

One economic request. Three ways to ask.

An agent wants to commit €1,180.00 to a twelve-month observability plan, against cost centre CC-OPS-114. That is above the policy’s €250 approval threshold, so the answer is not yes. 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. Nothing here executes a payment.


1 - MCP: observe_spend_request

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

The MCP server acts for the agent its credential resolves to, and that resolution happens here rather than on the caller’s machine. Nothing local can state which agent it is: the tool takes no agent parameter, and there is no environment variable that names one.

Tool call
→ tool: observe_spend_request
  {
    "recipientId": "rcp_01JQ8F7Z9K3M4N5P6Q7R8S9T0V",
    "amount": "1180.00",
    "category": "software",
    "commitmentType": "PURCHASE_ORDER",
    "purpose": "Observability plan, 12 months, renewal
                for the platform team.",
    "projectReference": "CC-OPS-114"
  }
Tool result
← result

  This would have been held for a person to
  decide.

  AMOUNT_REQUIRES_APPROVAL - €1,180.00 is at or above
  the €250.00 threshold, so a person has to approve it.

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

  https://…/app/requests/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": "1180.00",
  "category": "software",
  "commitmentType": "PURCHASE_ORDER",
  "purpose": "Observability plan, 12 months, renewal for the platform team.",
  "projectReference": "CC-OPS-114"
}
Response
201 Created

{
  "intent": {
    "id": "spi_01JQ8F7Z9K3M4N5P6Q7R8S9T0V",
    "mode": "SHADOW",
    "money": {
      "asset": "EUR",
      "amountMinor": "118000",
      "amount": "1180.00",
      "amountDisplay": "€1,180.00"
    },
    "category": "software",
    "commitmentType": "PURCHASE_ORDER",
    "projectReference": "CC-OPS-114"
  },
  "decision": {
    "outcome": "REQUIRE_APPROVAL",
    "explanation": "This would have been held for a
                    person to decide.",
    "reasons": [
      { "code": "AMOUNT_REQUIRES_APPROVAL",
        "message": "€1,180.00 is at or above the €250.00
                    threshold, so a person has to approve it." }
    ],
    "engineVersion": "policy-engine@1.1.0"
  },
  "url": "https://…/app/requests/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: '1180.00',
    category: 'software',
    commitmentType: 'PURCHASE_ORDER',
    purpose: 'Observability plan, 12 months, renewal for the platform team.',
    projectReference: 'CC-OPS-114',
  },
  // Stable across retries of this same order — not a fresh value each attempt.
  { idempotencyKey: 'procurement-2026-04-02-0031' },
);

if (decision.outcome !== 'ALLOW') {
  // Do not commit to it. Nothing was charged either way — Paygente
  // records the answer, it does not act on it.
  console.error(decision.explanation, url);
}

Authentication, honestly

What works today

An agent key, for an agent. Issue one from your sandbox under Agents and send it as Authorization: Bearer. It resolves server-side to one organization and one agent, and nothing the caller sends can widen that.

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

What does not exist yet

Service accounts, scopes and production authentication are not built. An agent key lives as long as the sandbox that issued it and carries one fixed permission set; a demo session is not a production credential. Neither is an account, and neither should be treated as one. Do not put real supplier data in the sandbox.