Blog · 2026-05-25

A webhook your agent can query

Store webhook events with denkops.store and read them back with an MCP tool. One backend gives agents a recent-events feed, payment example included.

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.

FAQ

Do I need a queue or a separate database to buffer webhook events for an agent?

No. denkops.store writes events to the durable /persist disk of your always-on slot, and an MCP tool reads them back with list and get. For a recent-events feed, the store plus a TTL replaces both the queue and the cleanup job.

How do I keep the tool's answer limited to recent events?

Set a TTL when storing each event, for example ttlSeconds: 86400 for a 24-hour window. Expired keys stop appearing in list results, so the tool returns only events inside the window without any manual pruning.

Can the payment provider reach my webhook if DenkOps requires an API key?

Yes. Auth is on by default, but you choose which paths are public. Make the webhook path public so the provider can POST to it, and verify the provider's HMAC signature in the handler so only genuine events get stored.

Ship it yourself: bunx denkops deploy or say "deploy on DenkOps" from your coding agent.

Start on DenkOps →

← Give your agent memory with denkops.store · Secrets your agent can use but never see →