--- title: "MCP guide: your casino" description: What the OneHazel adapter is, the checklist your casino needs ready before you start, and how to install, check, deploy and register the adapter. --- # Your casino OneHazel calls your casino to place bets, pay wins, read balances and look up players. Your casino already does all of this in its own system, its PAM. The OneHazel adapter is a small program that sits in front of your PAM. It accepts OneHazel's calls and checks they really come from OneHazel. Then it passes them to your PAM in your PAM's own format. Your PAM does not change. One adapter serves every module you run: `wallet`, `player`, and `webhook.receive` when a vendor needs it. It reads one settings file and runs as one container. Your agent is told the same, word for word: > One adapter process serves every module: one settings file and one container, with each module's own signing secret and callback key in its own variables. The full reference is on [The adapter](/connect/adapter), which also shows the older layout of one adapter per module; that still works. ::: tip Words on this page - **PAM**: Your casino's own player account system. It holds your players and their balances. - **JSONPath**: A short address, such as `$.data.balance`, that points at one value inside a response. - **dedup**: Short for deduplication: a message that arrives twice is acted on only once. - **X-OneHazel-Env**: A label on every OneHazel call. `test` means test players only; `live` means real players and real money. - **healthz**: An address on the adapter, `/healthz`, that answers 200 while the adapter is running. - **allowUnreachable**: An option that lets your agent register an address before the adapter runs there. - **provider_slug**: The name a game studio has in your casino's game catalogue, such as `acme-studio`. - **game_code**: The id your casino's game catalogue gives one game, such as `book-of-gold`. With `provider_slug` it names your test game. ::: ::: tip What you need before you start Have each of these ready. Your agent asks for them along the way. | Item | What it is | |---|---| | Your PAM's address | The web address of your PAM's provider routes. See the rule below. | | A PAM service token | The value your PAM expects in its `Authorization` header from a trusted service, for example `Bearer ...`. | | A host for the adapter | A server with Docker and a public https address that reaches the adapter's port. One way to get the address is in the box below, with Caddy. Another is a platform that gives each service its own public https URL. Your PAM's hosting team may already run one of these. | | A test player | A player id in your test setup, with money in its test balance. Your agent registers it as `testPlayerId`. | | A restricted test player | A second test player with an active limit, block or self-exclusion. Your agent registers it as `restrictedPlayerId`. | | One test game | A game that is active in your catalogue and accepted for test players: its `provider_slug` and `game_code`. The register step asks for it as `testGame`, and the wallet adapter's settings list it too. | | Redis | Only for `webhook.receive`. A Redis address, `redis://` or `rediss://`, that the adapter reads from the variable `ONEHAZEL_ADAPTER_REDIS_URL`. With the adapter in Docker, the simplest is a `redis` service in the same `docker-compose.yml`, with `ONEHAZEL_ADAPTER_REDIS_URL="redis://redis:6379"`; step 4 shows the three lines. It publishes no port, so only the adapter reaches it. The other option is a managed Redis service. An adapter started without Docker can use a Redis on its own host bound to localhost, `docker run -d --name onehazel-redis -p 127.0.0.1:6379:6379 redis:7`, at `redis://127.0.0.1:6379`. Whichever you choose, never publish its port on the public address: Redis has no password by default. See the rule below. | ::: ::: details Give the adapter a public https address (one option: Caddy) The adapter serves plain http on port 8080. Caddy is one free program that puts https in front of it and fetches a free TLS certificate for your name itself. On the adapter's host: 1. In your domain's DNS, add an A record, for example `adapter.yourcasino.com`, pointing at the host's public IP address. 2. Install Caddy with the Debian or Ubuntu lines on [caddyserver.com/docs/install](https://caddyserver.com/docs/install). It starts as a service named `caddy`. 3. Replace the contents of `/etc/caddy/Caddyfile` with these three lines, using your own name: ```text adapter.yourcasino.com { reverse_proxy 127.0.0.1:8080 } ``` 4. Run `sudo systemctl reload caddy`. Open ports 80 and 443 in the host's firewall. The `docker-compose.yml` publishes 8080 on `127.0.0.1` only, so the internet cannot reach it and Caddy, on the same host, reaches it on localhost. Your adapter's public address is then `https://adapter.yourcasino.com`. The certificate can take a minute to arrive after the DNS record is visible. ::: OneHazel creates a callback key for each module when your agent registers it. It sends that key on every call, so your adapter knows the call is genuine. If your casino already issues its own provider keys, your agent can register one of yours instead. ### Choosing the test game > testGame is a game that is ACTIVE in your own catalogue, identified by the same provider slug and game code your launch endpoint accepts. To pick one, ask the person who manages your game catalogue, or use any game a real player can open today. If it is wrong, the wallet probe fails with CONNECT_TEST_GAME_REJECTED and nothing is charged: your casino refuses the session before any bet. It is wallet-only: the player module needs no game and uses testPlayerId and restrictedPlayerId. ## The rules your agent follows Each rule below is quoted word for word from your agent's instructions. The plain-English version comes first. The PAM address must include the full path to the provider routes, not just the host name: > native.baseUrl is the FULL route prefix the adapter appends contract paths to (e.g. https://pam.example.com/provider/v1, not the bare host). One PAM token can serve every adapter, if your PAM allows that: > One token is enough when a single PAM service credential may call every wallet route, the player route and the event sink: set PAM_WALLET_TOKEN, PAM_PLAYER_TOKEN and PAM_WEBHOOK_TOKEN to the same value. Your agent proves the player lookup works before it registers anything: > Call your player route with the real testPlayerId first; it must answer 200. The player lookup must find a player by the same id your wallet uses. If your PAM looks players up some other way, the adapter can pass the id as a query parameter or search for your own id first: > Before registering, confirm your player read route accepts the same player id your wallet uses. If it does not, put {player_external_id} in a query parameter ("route": "/players?externalId={player_external_id}"), or add a search step that finds your internal id ("resolve": { "route": "/players/search?ref={player_external_id}", "idPath": "$.items\[0\].id" } with "route": "/players/{player_internal_id}"). The adapter reads the first testPlayerId through that route once at startup, and in --check when the PAM token is set; a 4xx on that read stops it. `webhook.receive` needs an address in your PAM to deliver events to, and Redis: > webhook.receive still needs a PAM-side sink endpoint and Redis for dedup in production, or the "url" dedup form with a shared store you provide in code; the in-memory store is refused in production unless webhook.allowMemoryDedup is true. Your agent never invents player or money data: > events.push: skip it unless connect_connector_requirements lists it; never fabricate player or financial activity. ## Step 1. Get the adapter The adapter is version `0.2.1`. Use the Docker image. It works whatever language your PAM is written in. This step only downloads it: the adapter cannot start until its settings file exists (step 2) and its values are in place (step 4). On your host, on a Mac, Linux or Windows: ```bash docker pull ghcr.io/zsdigitalmt/onehazel-connect-adapter:0.2.1 ``` **Without Docker.** If your PAM is written in Node.js, your engineers can install the same adapter from npm instead: ```bash npm install @onehazel/connect-adapter@0.2.1 ``` ::: warning Not on npm yet If npm answers no matching version, that version of @onehazel/connect-adapter is not published to npm yet: use the Docker image lines instead, they carry the same version. ::: ::: info What you should see For Docker, the image downloads and Docker prints `Status: Downloaded newer image` or `Status: Image is up to date`. Nothing is running yet. ::: ## Step 2. Let your agent write the settings file Your agent writes one settings file, `onehazel-adapter.json`, with a section for each module under `modules`. Each section names that module's own secret and key, because OneHazel gives each module its own. It never writes a secret into the file, only the names of variables. The PAM address is the full path to the provider routes: > native.baseUrl is the FULL route prefix the adapter appends contract paths to (e.g. https://pam.example.com/provider/v1, not the bare host). Your agent needs one real answer from your PAM to point each setting at the right value in it: > The sample JSONPaths are placeholders: fetch ONE real response for the test player and write every path against it; a wrapped response needs $.data.\* paths. Your agent never holds your PAM token, so it does not call your PAM itself. It asks you for that answer: > You never hold the PAM token, so never call the PAM yourself: ask your human to run one request for the test player on the adapter's host, with the token from onehazel.env, and paste the response body with tokens and personal values removed. The adapter's --check with the token set only proves the player route answers: it prints the status, not the body. On the adapter's host, make a folder for the adapter and create `onehazel.env` in it with one line for now, your real PAM token in place of the example. Step 4 adds the other lines to the same file, and explains the quotes: ```text PAM_PLAYER_TOKEN="Bearer your-pam-service-token" ``` Then run one request for your test player from that folder. Use your PAM's address, its player route, the header it expects and your test player's id. In bash on the adapter's host: ```bash set -a; . ./onehazel.env; set +a curl -s -H "Authorization: $PAM_PLAYER_TOKEN" "https://pam.yourcasino.com/provider/v1/players/" ``` Remove every token, name, email and other personal value from the output, then paste the rest into the chat. The adapter's config check in step 3 confirms the player route answers once the token is set, but it prints only the status, never the body. With only one PAM, test and live use the same address: > If you have one PAM environment, register baseUrls with test and live both at the adapter URL and set native.baseUrl.test and .live to the same PAM prefix. The `wallet` and `player` sections list your test player ids as `testPlayerIds`. OneHazel's test calls are refused for any other player: > testPlayerIds is required whenever modules includes wallet or player: list your test player ids, because X-OneHazel-Env: test is refused for any other player with player_unknown and the message "env_refused: player not in testPlayerIds". The `wallet` section also names your test game as `testGame`, the same game you register: > Name one active game in your catalogue that may be used for test rounds (provider slug and game code as your PAM knows them) and register it with the wallet as testGame: { provider_slug, game_code }. ::: info What you should see One file, `onehazel-adapter.json`, in your project. It mentions variable names such as `PAM_WALLET_TOKEN`, never a token itself. Its `wallet` section has `testPlayerIds` and `testGame`; its `player` section has `testPlayerIds`. ::: ## Step 3. Run the config check Before you deploy, check the settings file. The check reads the file and lists everything wrong with it, each problem with the setting it is about. It validates the file without secrets: the variables may be unset. It never starts the adapter. The player route probe is extra: it runs only when `PAM_PLAYER_TOKEN` is set, and then reads your first test player once. The lines below leave it out. On a Mac or Linux host with Docker: ```bash docker run --rm -e CONNECT_ADAPTER_CHECK=1 \ -v "$PWD/onehazel-adapter.json:/etc/connect-adapter/adapter.json:ro" \ ghcr.io/zsdigitalmt/onehazel-connect-adapter:0.2.1 ``` On a Windows host, in PowerShell: ```powershell docker run --rm -e CONNECT_ADAPTER_CHECK=1 ` -v "${PWD}/onehazel-adapter.json:/etc/connect-adapter/adapter.json:ro" ` ghcr.io/zsdigitalmt/onehazel-connect-adapter:0.2.1 ``` Or, with Node.js installed: ```bash npx -y @onehazel/connect-adapter@0.2.1 --check --config onehazel-adapter.json ``` ::: warning Not on npm yet If npm answers no matching version, that version of @onehazel/connect-adapter is not published to npm yet: use the Docker image lines instead, they carry the same version. ::: To run the player route probe too, run the check with the token loaded from `onehazel.env`. In bash on the adapter's host: ```bash set -a; . ./onehazel.env; set +a docker run --rm -e CONNECT_ADAPTER_CHECK=1 -e PAM_PLAYER_TOKEN \ -v "$PWD/onehazel-adapter.json:/etc/connect-adapter/adapter.json:ro" \ ghcr.io/zsdigitalmt/onehazel-connect-adapter:0.2.1 ``` ::: info What you should see `is valid`. A file that is not valid prints `is INVALID` and one line per problem, such as `errors.shape: must be { code, message } ... (rule: required)`. Give those lines to your agent, let it fix the file, and run the check again. Do not deploy a file that fails: the adapter refuses to start with it. ::: ## Step 4. Deploy and check it answers **This step is yours.** In short: you start the adapter, check it answers, and only then does your agent register it. Word for word: > Order: check config, deploy, probe healthz, register. Run the snippet's --check line on every config until each exits 0, then ask your human to run the snippet's Deploy section on the host (deploying the adapter is a human step), confirm both /healthz probes answer 200, then register. Your agent gives you a `docker-compose.yml` with one service for every module: it is the Deploy section of the adapter install text your agent gets from OneHazel's `connect_samples` tool (`framework: "adapter"`). Copy it to a folder on your host, with the settings file beside it. **Put the values in an env file.** Open the `onehazel.env` you created in step 2, keep it in the same folder as `docker-compose.yml`, and replace its contents. It gets one `NAME=value` line per variable the `docker-compose.yml` names, for example `PAM_WALLET_TOKEN="Bearer ..."`. Use double quotes around a value that contains a space. If a value contains a `$`, use single quotes instead, for example `PAM_WALLET_TOKEN='Bearer ab$cd'`: inside double quotes both bash and Docker Compose replace `$cd` with an empty value, so the adapter starts with a wrong token. The install text says the same: > Quote every value that contains a space, for example PAM_WALLET_TOKEN="Bearer ...". A value that contains a $ goes in single quotes instead, for example PAM_WALLET_TOKEN='Bearer ab$cd': inside double quotes bash and Docker Compose both expand $name, so the token silently changes and the adapter still starts. Compose and a shell that loads the file with set -a read both forms. `docker compose config` shows a single-quoted `$` as `$$`. That is only how Compose prints it: the container receives a single `$`, so do not edit the file. Only you and the adapter should read it, and it never goes into your code. The signing secret and callback key do not exist until your agent registers in step 5, so start with stand-in values. Word for word: > Before the first register the module's ONEHAZEL_ signing secret and callback key do not exist yet: start the adapter with stand-in values so /healthz answers (the signing secret must be whsec, an underscore and any letters; the callback key any text), then after connect_register_endpoint put the real values in the env file and restart the adapter (with Docker, run the start line again plus --force-recreate; without Docker, stop the adapter process and run its npx start line again with the new values in the environment). Before the first register, `onehazel.env` looks like this for all three modules. Keep only the lines for the modules you run. The stand-in values are not secrets; your real PAM tokens and Redis address are: ```text ONEHAZEL_WALLET_SIGNING_SECRETS="whsec_standin" ONEHAZEL_WALLET_CALLBACK_KEY="standin" PAM_WALLET_TOKEN="Bearer your-pam-service-token" ONEHAZEL_PLAYER_SIGNING_SECRETS="whsec_standin" ONEHAZEL_PLAYER_CALLBACK_KEY="standin" PAM_PLAYER_TOKEN="Bearer your-pam-service-token" ONEHAZEL_WEBHOOK_SIGNING_SECRETS="whsec_standin" ONEHAZEL_WEBHOOK_CALLBACK_KEY="standin" PAM_WEBHOOK_TOKEN="Bearer your-pam-service-token" ONEHAZEL_ADAPTER_REDIS_URL="redis://redis:6379" ``` **Redis for `webhook.receive`.** The last line above points at a `redis` service in the same `docker-compose.yml`. Add these lines under `services:`, beside `onehazel-adapter`: ```yaml redis: image: redis:7 restart: unless-stopped volumes: - redis-data:/data ``` Add these lines at the top level of the file too, not under `services:`: ```yaml volumes: redis-data: ``` It publishes no port, so only containers started from that file reach it, by the name `redis`. The named volume keeps the webhook dedup keys. Without it, `docker compose down` deletes them. With a managed Redis instead, skip these lines and put its `rediss://` address on that line. **Start it.** On a Mac or Linux host with Docker: ```bash chmod 600 onehazel.env docker compose --env-file onehazel.env up -d ``` On a Windows host, in PowerShell, keep `onehazel.env` in a folder only you can open, then: ```powershell docker compose --env-file onehazel.env up -d ``` The other commands this page runs on the host are written for bash: the PAM request in step 2, the config check with the probe in step 3, the start without Docker, and both `/healthz` checks below and in step 5. They do not run in PowerShell. On a Windows host, run them in a bash shell: WSL or Git Bash. Save `onehazel.env` with LF line endings: Notepad saves CRLF, and bash then keeps a carriage return at the end of every value, so the token is wrong even though Compose reads the file fine. To check or convert it, run `dos2unix onehazel.env` in that shell, or choose LF in your editor's line ending setting. **Without Docker.** Load the same variables into the shell from `onehazel.env`, then start the adapter. In bash on the adapter's host: ```bash set -a; . ./onehazel.env; set +a npx -y @onehazel/connect-adapter@0.2.1 --config onehazel-adapter.json --port 8080 ``` ::: warning Not on npm yet If npm answers no matching version, that version of @onehazel/connect-adapter is not published to npm yet: use the Docker image lines instead, they carry the same version. ::: Then run two checks: one on the host, one through the adapter's public address. In bash on the adapter's host: ```bash curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/healthz curl -s -o /dev/null -w "%{http_code}\n" https:///healthz ``` Replace `` with the adapter's public address. ::: info What you should see `200` from both lines. Fix anything else before step 5. When the first line fails, read the adapter's output on the host: ```bash docker compose logs onehazel-adapter ``` `onehazel-adapter` is the service name in your `docker-compose.yml`. A line that starts `connect-adapter: refusing to start:` names what is wrong, such as a missing variable. [Troubleshooting](/connect/mcp/troubleshooting#the-adapter-will-not-start) lists each one. When only the second line fails, the adapter runs but your public address does not reach it. ::: ## Step 5. Register Tell your agent both checks answered 200. It registers the adapter's public address once for each module: the same address every time. OneHazel checks the address answers before it saves it: > Deploy your implementation (or the adapter) first and curl its /healthz yourself, THEN register: connect_register_endpoint probes GET <baseUrl>/healthz and stores nothing on a connection error, timeout or 5xx; pass allowUnreachable: true only to register before deploying. Registering shows your agent two secret values once. It writes them to a private file and tells you only where: > Write the values straight into <project>/.onehazel/<module>.env (mode 0600, gitignored) or the deployment secret store, then tell the human the path and the variable names. Add .onehazel/\*.env to .gitignore before the first write. Your agent registers every module first, so it writes one env file per module under `.onehazel/`, for example `.onehazel/wallet.env` and `.onehazel/player.env`. When it has registered them all, it asks you to copy the values. Copy the values from each file into `onehazel.env` on your host, in place of the stand-in values. Then restart the adapter once, with the same start line plus `--force-recreate`: ```bash docker compose --env-file onehazel.env up -d --force-recreate ``` This recreates the `redis` service too. Its named volume from step 4 keeps the webhook dedup keys. **Without Docker.** Stop the adapter with Ctrl+C in the terminal where it runs, then start it again with the new values. In bash on the adapter's host: ```bash set -a; . ./onehazel.env; set +a npx -y @onehazel/connect-adapter@0.2.1 --config onehazel-adapter.json --port 8080 ``` ::: warning Not on npm yet If npm answers no matching version, that version of @onehazel/connect-adapter is not published to npm yet: use the Docker image lines instead, they carry the same version. ::: Run the two `/healthz` checks from step 4 again and tell your agent they answered 200. Until you do, every test call is refused with 401 `unauthorized`, because the adapter still holds the stand-in values. So your agent runs no test before you confirm. Its instruction, word for word: > Ask your human to copy every newly registered module's values from .onehazel/<module>.env into onehazel.env on the adapter's host and restart the adapter; run conformance only after they confirm, or every call answers 401 against the stand-in values. ::: info What you should see The **Module status** card shows each module as **Registered**. Your agent tells you the file paths and the variable names, never the values. After you confirm the restart, it starts a test run for each module. ::: Made a mistake in the address, a test player or the test game? Your agent registers the module again with the right value. That changes only OneHazel's registration and keeps the module's secrets, so you copy nothing and do not restart the adapter. If your agent also changes `testGame` in `onehazel-adapter.json`, restart the adapter: it reads that file only when it starts. Word for word: > Registering a module that is already registered keeps its signing secret and callback key (the result says secretUnchanged: true and carries no secret), so fixing an address, a test player or the test game this way changes only OneHazel's registration and needs no adapter restart; only connect_rotate_secret, or a different callbackKey, replaces them. For a wrong address, word for word: > Registered the wrong URL? Call connect_register_endpoint again with the right one: the secrets stay the same. Unregister only to remove a module, because registering it again after that mints new values your human must copy into onehazel.env before restarting the adapter. Its conformance runs from before the unregister no longer count: connect_status shows the module as registered, and the journey asks your human for that restart before a new run. In the app, the **Remove** button on the **Module status** card is the same unregister, and its confirmation says the same. So after an unregister and a new register, the **Module status** card shows that module as **Registered**, not **Conformant**, even if it passed before. Copy its new values from `.onehazel/.env` into `onehazel.env`, restart the adapter as above, and confirm both `/healthz` checks answer 200. Your agent then runs a new test for it. A secret that was ever shown is replaced: > If a secret was ever printed, logged or committed, call connect_rotate_secret for that module with what "both" (or "callback_key" or "signing_secret" for one of them); the previous values stay accepted for 1 hour, so update the adapter within that hour. ## Next Back to [the journey](/connect/mcp/journey), step 4: conformance.