Appearance
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, which also shows the older layout of one adapter per module; that still works.
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.
testmeans test players only;livemeans 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. Withprovider_slugit names your test game.
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. |
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:
- In your domain's DNS, add an A record, for example
adapter.yourcasino.com, pointing at the host's public IP address. - Install Caddy with the Debian or Ubuntu lines on caddyserver.com/docs/install. It starts as a service named
caddy. - Replace the contents of
/etc/caddy/Caddyfilewith these three lines, using your own name:
text
adapter.yourcasino.com {
reverse_proxy 127.0.0.1:8080
}- Run
sudo systemctl reload caddy. Open ports 80 and 443 in the host's firewall. Thedocker-compose.ymlpublishes 8080 on127.0.0.1only, 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.1Without 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.1Not 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.
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/<test player id>"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 }.
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.1On 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.1Or, with Node.js installed:
bash
npx -y @onehazel/connect-adapter@0.2.1 --check --config onehazel-adapter.jsonNot 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.1What 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:/dataAdd 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 -dOn a Windows host, in PowerShell, keep onehazel.env in a folder only you can open, then:
powershell
docker compose --env-file onehazel.env up -dThe 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 8080Not 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://<adapter public URL>/healthzReplace <adapter public URL> with the adapter's public address.
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-adapteronehazel-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 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-recreateThis 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 8080Not 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.
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/<module>.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, step 4: conformance.