The pattern is one backend with two doors. A webhook endpoint writes every incoming event to denkops.store, and an MCP tool reads them back, so "what happened recently?" becomes a question any assistant can answer. Your payment provider POSTs to the first door, your agent calls the second, and both live in the same small app.
Why does this need a backend at all?
Webhooks push, agents pull. A payment provider fires an event once, at the moment it happens, at a URL it can reach. An assistant asks about it minutes or hours later, in the middle of a conversation. Something has to sit between those two timelines and hold the events, and that something needs a stable URL and a durable place to write. An always-on DenkOps slot with the built-in store on /persist is exactly that, with no queue or extra service in between.
What does the code look like?
One Bun file. The webhook handler stores each event under a timestamped key with a 24-hour TTL, and the MCP tool lists that prefix and returns the events newest first:
import { Hono } from "hono";
import denkops from "@denkopsai/sdk";
import { defineMcp } from "@denkopsai/sdk/mcp";
const mcp = defineMcp({
name: "payments-feed",
version: "1.0.0",
tools: {
recent_payments: {
description: "Payment events from the last 24 hours, newest first.",
handler: () => {
const keys = denkops.store.list("evt:").sort().reverse();
return keys
.map((key) => denkops.store.get(key))
.filter((raw): raw is string => raw !== null)
.map((raw) => JSON.parse(raw));
},
},
},
});
const app = new Hono();
app.post("/hooks/payments", async (c) => {
const event = await c.req.json();
denkops.store.set(
`evt:${Date.now()}:${event.id}`,
JSON.stringify(event),
{ ttlSeconds: 24 * 60 * 60 },
);
return c.json({ ok: true });
});
app.route("/", mcp); // serves POST /mcp next to the webhook
export default { port: 3000, fetch: app.fetch };Two details do the heavy lifting. The millisecond timestamp in the key means a plain string sort is a time sort, so "newest first" is one reverse(). And the TTL is the retention policy: events older than 24 hours disappear from list on their own, so there is no cleanup job to write and the tool's answer is always fresh by construction.
Deploy it and you get one URL with both doors: https://payments-feed.denkops.host/hooks/payments for the provider, /mcp for every assistant.
How does auth work when a provider and an agent share one app?
DenkOps endpoints require the app's API key by default, and a payment provider will not send your API key. So you make the /hooks/payments path public and verify the provider's signature inside the handler instead, the same raw-body HMAC check from the Bun + Hono webhook guide. The MCP side stays protected: keep the API key for your own agents, or flip on connector OAuth when teammates should install it in their assistant.
The same shape works for any event source. Form submissions, CI results, support tickets: if it can POST, your agent can query it.