Exposing payment endpoints through MCP takes a week. Deciding what an agent is allowed to do with them takes the real engineering.
Created: 2026-11-12 | Status: Draft, pending partner review | Vertical: Fintech / B2B payments (CMO intel 2026-11-12)
A fintech with a clean REST API can wrap it in an MCP server in a few days. The tool list looks right, the demo works, and a customer's agent pays an invoice on the first try. Then the security review asks one question: when the agent calls create_payment, whose authority is it using, and what stops it from paying the wrong supplier twice?
Most teams discover they have no answer. Their API was built for a human behind a session or a backend service with an API key. Neither model describes software that acts on behalf of a user, makes its own decisions about what to call, and retries when it gets confused.
A typical B2B payments API authenticates a company with a long-lived key and authorizes by role. Admin can create payments, read-only cannot. That works when the caller is a deterministic ERP integration that does the same thing every night.
An agent breaks three assumptions at once. It is non-deterministic, so the same invoice may produce different call sequences on different days. It is driven by untrusted input, because the invoice PDF it reads may contain text that tries to steer it. And it acts for a human who never saw the specific payment, so the audit trail "key X called endpoint Y" is useless to a compliance officer asking who approved this.
In the EU the regulatory context makes this concrete. PSD2's strong customer authentication rules require that the authentication code be dynamically linked to the amount and the payee (Commission Delegated Regulation 2018/389, Article 5). DORA has applied to financial entities since January 2025 and puts ICT third-party risk and incident traceability on the regulated firm, which includes the vendors it wires into its payment flow. "The agent decided" will not survive either regime.
The pattern is consistent across fintech and payments-adjacent SaaS. The first version maps every endpoint to a tool, reuses the customer's existing API key, and relies on the model to behave. It passes demos. It fails in review, or worse, in the first week of production use, because there is no place in the system where "this payment needs a human" can be expressed.
The fix is rarely a rewrite. It is a new layer between the agent-facing tool surface and the payment core, and it touches identity, policy, and the audit log. That is three areas, usually owned by three different people, which is why it stalls. The identity work sits with platform. The policy work sits with whoever owns risk. The audit work sits with whoever answers to the regulator. Nobody owns the seam, and the backend team that would build it is already committed to the roadmap.
We recommend four pieces, built in this order.
First, issue a separate credential per delegation, not per customer. Do not hand the agent the customer's API key. Issue a short-lived token that records the human principal, the agent identity, and a scope. MCP's HTTP transport already defines an OAuth 2.1 based authorization flow, so use it rather than inventing a header. For scoped, structured permissions, OAuth Rich Authorization Requests (RFC 9396) let you carry fields such as maximum amount and permitted payees inside the token itself.
Second, evaluate policy server-side on every write, outside the model. The MCP server must not trust the agent to respect limits it was told in a prompt. Put the check in code the model cannot reach:
type Delegation = {
principalId: string;
agentId: string;
maxAmountMinor: number; // per payment, in cents
dailyCapMinor: number;
allowedPayeeIds: Set<string>; // pre-approved suppliers only
expiresAt: Date;
};
async function authorize(d: Delegation, p: PaymentIntent): Promise<Decision> {
if (d.expiresAt < new Date()) return deny('delegation_expired');
if (!d.allowedPayeeIds.has(p.payeeId)) return requireApproval('new_payee');
if (p.amountMinor > d.maxAmountMinor) return requireApproval('over_limit');
const spent = await ledger.spentToday(d.principalId, d.agentId);
if (spent + p.amountMinor > d.dailyCapMinor) return requireApproval('daily_cap');
return allow();
}
Note the third outcome. Most decisions should not be allow or deny. They should be requireApproval, which parks the intent and sends a human a request showing the exact amount and payee. That approval, not the agent's call, is what you link to the payment for SCA purposes.
Third, key idempotency on intent, not on request. Agents retry, and they often retry with slightly different parameters. If your idempotency key is a client-supplied header, two retries with different keys create two payments. Derive the key from the business intent instead, for example a hash of principal, payee, invoice reference, and amount. A second attempt for the same invoice then collapses into the first, whatever the agent does.
Fourth, keep an append-only record that links request, policy decision, approval, and payment. Store the tool call, the delegation it ran under, the policy result with the rule that fired, the approver if any, and the resulting payment ID in one correlated trail. In Postgres this can be a plain insert-only table with a revoked UPDATE and DELETE grant for the application role. When an auditor asks why a payment happened, you answer with one query.
Each of these pieces is a few weeks of work for an experienced backend engineer. Together they touch the API gateway, the auth service, the payment core, and the audit store. In a Java/Kotlin and Spring Boot estate with Kafka between services, which is common for growth-stage payments companies, that means changes in four codebases with four sets of reviewers.
Teams that handle this well do not pull the core payments squad off their roadmap. They stand up a small group with one explicit mandate, the agent authorization layer, staffed with backend engineers who already know the stack and one person with identity experience. That group owns the seam. The core team reviews, but does not build.
This is the staffing shape we help clients create. Our first client started with one engineer and grew into a complete cross-functional team the same way: one scoped mandate, one person who knew the stack, then more people as the surface area became clear. We hire for the specific stack, whether that is Spring Boot and Kafka or TypeScript on Node, and embed the engineers in the client's repos and review process from the first week.
requireApproval the default outcome for anything outside pre-approved payees and amounts.
SifrVentures builds dedicated engineering teams for tech companies. Based in Berlin. Learn how we work | Read more on our blog