# Add an Arena54 webhook receiver to this project

Goal: Receive Arena54’s webhooks in this project, check each one is real, and act on it once.

You are a coding agent working inside my project. Do the work. Do not ask me anything you can find out by reading the project. Do not invent an endpoint, a field or an error code that is not written below.

## 1. Look at the project first

- Find the framework and language, how the project is run and tested, and where the project defines HTTP routes, and how it reads the raw request body before any JSON parsing.
- Find how the project reads configuration and environment variables, and which files git ignores.
- Any backend or script will do. Use the HTTP client and the router the project already has, or the runtime’s own if it has none. If the project has nowhere to run code off the page, stop and tell me.
- If the project has no environment example file, add one, and load the environment the way the runtime provides.
- If the project already has an Arena54 client or key variable, reuse it.
- Tell me in two or three lines what you found and what you will change, then carry on.

## 2. The contract

Arena54 sends a `POST` with a JSON body to an https address of mine when something changes. There is nothing to call; this project only receives.

Events:

- `listing.published`: A game, studio or person was approved and is now listed.
- `listing.updated`: A published listing was changed.
- `tip.paid`: A player tipped one of your registered games through POST /v1/play/tips. Sent only to that game’s maker.
- `ranking.moved`: A listing moved on the weekly board. Checked once a day, at 04:00 UTC.
- `ping`: a test event sent from the Arena54 dashboard.

The body:

```json
{ "id": "d856e71f-…", "event": "listing.updated", "created_at": "2026-10-12T19:34:21.387Z", "data": { "kind": "game", "slug": "lagos-run", "name": "Lagos Run", "url": "https://arena54.com/g/lagos-run", "api": "/games/lagos-run" } }
```

`data` for a listing event is `kind`, `slug` or `handle`, `name`, `url`, `api`. For `ranking.moved` it is `kind`, `board`, `name`, `url`, `rank`, `previous_rank`. For `ping` it is `message`.

Headers on every delivery: `x-arena54-event`, `x-arena54-delivery`, `x-arena54-timestamp`, `x-arena54-signature`.

`x-arena54-timestamp` is Unix time in whole seconds. `x-arena54-signature` is bare lowercase hex, with no prefix.

The signature is HMAC-SHA256 over the timestamp, a dot, and the body exactly as received: `hmac(secret, timestamp + "." + rawBody)`. The secret starts `a54whsec_` and is shown once in the Arena54 dashboard. Read it from the environment variable `ARENA54_WEBHOOK_SECRET`.

`data.api` is a path under `https://developers.arena54.com/api/v1`. Reading it needs an Arena54 key, sent as `Authorization: Bearer <key>` from the environment variable `ARENA54_API_KEY`. Only a live key returns real listings; a test key returns sample ones, so a real listing answers 404 with it.

Delivery: any 2xx within 8 seconds is success. Anything else is tried again, up to 6 times in all, with the same `id` each time. A redirect is not followed.

## 3. Do this, in order

1. Add ARENA54_WEBHOOK_SECRET to the project’s environment example file with an empty value.
2. Add one POST route, for example /webhooks/arena54. Read the raw body as text before anything parses it.
3. Refuse with 401 when the timestamp header is missing or more than 300 seconds from now, or when the signature does not match. Compare in constant time. A signed body that is not JSON gets a 400.
4. Then parse the JSON. If you have already handled this `id`, answer 200 and do nothing. Keep handled ids somewhere that survives a restart.
5. Answer 200 at once and do slow work after answering.
6. Handle each event I care about: for a listing event, fetch `data.api` for the listing as it is now, if I have given the project an API key; otherwise log the event. Log `ranking.moved` and `ping` unless I ask for more. Ignore events you do not know, with a 200.
7. Tell me the full address of the route, so I can add it in the Arena54 dashboard under Webhooks.

## 4. Prove it works

Set `ARENA54_WEBHOOK_SECRET` to any long string for a local test, and run the project.

- Write a small script that builds a body, signs it as above with the current time, and POSTs it to the local route. Expect HTTP 200.
- Send the same request with one character of the body changed. Expect HTTP 401.
- Send it with a timestamp ten minutes old and a matching signature. Expect HTTP 401.
- Send the first request twice. Expect 200 both times, and the work done once.
- After I add the address in the dashboard, I will press “Send a test event”. A `ping` should arrive and show as Delivered there.

Show me the output of each check.

## 5. Rules you must not break

- Verify before you trust anything in the body.
- Use the raw body for the signature, never a re-serialised one.
- The signing secret never reaches a page or a commit.
- Never do slow work before answering: a late answer is a failed delivery and comes again.
- An event says that something changed, not everything about it. Read the current state from the API.

## 6. Done when

- The route verifies the timestamp and the signature.
- A repeated delivery is handled once.
- Unknown events are answered 200 and ignored.
- The four local checks give the answers above.

Then give me a short list of the files you changed and anything I must do by hand.

For a person, not needed for this task: https://developers.arena54.com/docs · This prompt: https://developers.arena54.com/prompts/webhooks.server.md
