---
title: "Trigger a Self-Hosted AI Agent with Webhooks (No Public IP) | WebhookRelay"
meta:
  "og:description": "Run a local AI agent built with Vercel's eve framework on a Raspberry Pi or Mac mini behind NAT, and trigger it with GitHub, Stripe or any webhooks via Webhook Relay — no port forwarding, no public IP."
  "og:title": "Trigger a Self-Hosted AI Agent with Webhooks (No Public IP)"
  description: "Run a local AI agent built with Vercel's eve framework on a Raspberry Pi or Mac mini behind NAT, and trigger it with GitHub, Stripe or any webhooks via Webhook Relay — no port forwarding, no public IP."
---

![Stripes](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/stripes.svg)

# **Trigger a Self-Hosted AI Agent with Webhooks (No Public IP)**

Run a local AI agent built with Vercel's eve framework on a Raspberry Pi or Mac mini behind NAT, and trigger it with GitHub, Stripe or any webhooks via Webhook Relay — no port forwarding, no public IP.

![Self-hosted AI agent on a Raspberry Pi receiving webhooks](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/blog/eve-agent/cover.jpg)

Self-hosted AI agents are having a moment. A Mac mini or Raspberry Pi is plenty to run an always-on agent — the model is an API call away, and everything that matters (your tools, your data, your state) stays on hardware you own.

But a **local AI agent** that can only be poked over the LAN misses the point of being always-on. The interesting work arrives as events from the outside world: a GitHub issue is opened, a Stripe payment fails, Grafana fires an alert. Those are **webhooks** — and GitHub can't POST to a box behind NAT with no public IP. Opening a port and pointing dynamic DNS at your home network is exactly the kind of thing you shouldn't do to the machine that runs an agent with shell access.

We built a small, fully working example that solves this properly: [**webhookrelay-eve-agent-example**](https://github.com/webhookrelay/webhookrelay-eve-agent-example) — an agent built with [eve](https://vercel.com/eve), Vercel's filesystem-first framework for durable agents, triggered by any webhook from anywhere, with zero inbound ports.

## [The architecture](#the-architecture)

![Self-hosted AI agent architecture: webhooks cross the NAT/firewall network boundary from Webhook Relay Cloud to a webhookrelayd sidecar and eve agent running on a home network or office LAN](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/blog/eve-agent/architecture.svg)

[Webhook Relay](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/) provides the stable public HTTPS endpoint. The `webhookrelayd` sidecar runs next to the agent in docker-compose and keeps an _outbound_ connection to it, so every webhook is pulled down to the agent over the internal compose network. The box stays invisible to the internet — this is the same [webhook forwarding](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/features/webhook-to-internal-server/) that powers local webhook development, used as permanent infrastructure.

## [The agent: three small files](#the-agent-three-small-files)

![eve agent project structure — instructions.md, agent.ts and a tools directory — powered by Webhook Relay](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/blog/eve-agent/eve-webhookrelay.png)

eve agents are just files. The model config points at any AI SDK provider — the example calls Claude through [Lightning AI](https://lightning.ai)'s Anthropic-compatible API:

```
// agent/agent.ts
const lightning = createAnthropic({
  baseURL: process.env.LIGHTNING_BASE_URL ?? "https://lightning.ai/v1",
  apiKey: process.env.LIGHTNING_API_KEY ?? "",
});

export default defineAgent({
  model: lightning(process.env.LIGHTNING_MODEL ?? "claude-sonnet-4-6"),
});
```

A custom **channel** gives the agent its webhook endpoint. Each delivery starts a fresh, durable agent session with the payload (and the provider headers Webhook Relay preserves, like `x-github-event`) as the message:

```
// agent/channels/webhook.ts
export default defineChannel({
  routes: [
    POST("/eve/v1/webhook", async (req, { send }) => {
      const body = await req.text();
      const session = await send(
        \`A webhook just arrived. Triage it and save a report.\n\n${body}\`,
        { auth: null, continuationToken: \`webhook-${crypto.randomUUID()}\` },
      );
      return Response.json({ ok: true, sessionId: session.id });
    }),
  ],
});
```

And a typed **tool** is what the model does about it — here, writing a triage report to disk. This is the part you'd swap for a Slack message, a database insert or a home-automation call:

```
// agent/tools/save_report.ts
export default defineTool({
  description: "Save a triage report for a webhook event as a markdown file.",
  inputSchema: z.object({ slug: z.string(), markdown: z.string() }),
  async execute({ slug, markdown }) { /* write reports/<date>-<slug>.md */ },
});
```

## [What it does](#what-it-does)

Send a test event to your bucket's public URL:

```
curl -X POST https://xxxxx.hooks.webhookrelay.com/ \
  -H 'content-type: application/json' \
  -H 'x-github-event: issues' \
  -d '{"action":"opened","issue":{"number":42,"title":"Payments webhook drops events during deploys", ...}}'
```

A few seconds later there's a report in `reports/`. This is real output from the example agent:

> **GitHub Issue Opened — acme/shop #42** · Severity: info A new GitHub issue was opened on the `acme/shop` repository by `octocat`. Although classified as `info`, the subject matter is noteworthy — dropped payment webhook events during deploys could indicate a reliability risk worth investigating proactively._Suggested next action:_ review and assign issue #42 to the payments team; investigate whether there is a known deploy-window gap in webhook event handling.

Point GitHub, Stripe, Shopify or your monitoring at the same URL and every event flows through — the agent reads the payload, decides a severity and writes up what happened and what to do next.

## [Running it on a Raspberry Pi or Mac mini](#running-it-on-a-raspberry-pi-or-mac-mini)

The whole stack is one `docker compose up --build -d` — the agent container plus the `webhookrelayd` sidecar. The image is built on multi-arch `node:24-slim`, so the same compose file works on a Raspberry Pi (arm64), a Mac mini acting as a quiet home AI server, a NAS or a VM behind a corporate firewall.

Two things worth turning on for hardware that lives at home:

- **[Durable retries](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/features/durable-retries/)** on the bucket — if the machine sleeps, reboots or loses connectivity, events queue in the cloud and deliver when the sidecar reconnects, for up to 30 days.
- A `WEBHOOK_SECRET` (supported by the example) so the agent's channel rejects anything that didn't come through your relay.

The [README](https://github.com/webhookrelay/webhookrelay-eve-agent-example) has the full quick start: create a bucket with an internal output pointing at `http://agent:3000/eve/v1/webhook`, drop your token into `.env.local`, and compose up. For local development you can skip Docker entirely and forward the bucket to `eve dev` with the [relay CLI](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/docs/installation/cli/).

The example triages events because it's a useful default that needs no extra API keys — but the wiring is the point. Swap `agent/instructions.md` and the tools, and the same skeleton becomes a code-review agent, an on-call assistant or the brain of your home automation: any webhook in the world can now reach the agent in your drawer.