Appearance
For the operator's engineer starting a module from a generated sample.
Samples guide
Every sample is a complete, runnable module for one stack and one module, generated from the Operator Contract. Start from one, replace its stores with your own, and it already does the parts that are easy to get wrong.
Generated, do not edit here
The sample pages are generated from templates and the contract. They are not edited by hand in this repository, and an edit to a sample page is overwritten the next time they are generated. Copy a sample into your own repository and change it there.
What each sample contains
| Stack | wallet | player | webhook.receive | payments.intent |
|---|---|---|---|---|
| TypeScript + Express | wallet | player | webhook.receive | payments.intent |
| TypeScript + Fastify | wallet | player | webhook.receive | payments.intent |
| Python + FastAPI | wallet | player | webhook.receive | payments.intent |
| PHP (no framework) | wallet | player | webhook.receive | payments.intent |
| PHP + Laravel | wallet | player | webhook.receive | payments.intent |
Each wallet and player sample:
- checks the
Authorization: Bearercallback key, then verifiesX-OneHazel-Signature(t=<ts>,v1=<hex>, 300 s tolerance, constant-time compare) over the raw body before parsing, then readsX-OneHazel-Env; - keeps a separate
testledger seeded with a funded test player, so conformance traffic never touches theliveone; - answers errors with the flat
{ code, message, retryable }envelope and handles money as integer minor units; - for wallet, keeps an idempotency store that returns the original response with
duplicate: trueon a replay; - for player, also seeds a restricted test player.
Each webhook.receive sample verifies X-OneHazel-Signature: sha256=<hex> over the raw body, deduplicates on deliveryId, and acknowledges before doing slow work.
Each payments.intent sample is the payment status receiver: the same sha256= check, then a required X-OneHazel-Env, deduplication on deliveryId, and a settled deposit credited once per intent_id. Creating intents is a call your cashier makes to OneHazel, so no sample serves it.
The same bytes are served to a coding agent by the connect_samples MCP tool, so a person and an agent start from the same code.
Run one locally
Each sample page has a Run it section with the exact commands for its stack. The pattern is the same everywhere: install the framework, then start the module with the two values you were shown at registration. For a first local run, throwaway values are enough:
bash
ONEHAZEL_CALLBACK_KEY=local ONEHAZEL_SIGNING_SECRET=local npx tsx onehazel-wallet.ts| Variable | Used by | Meaning |
|---|---|---|
ONEHAZEL_CALLBACK_KEY | wallet, player | the callback key shown once at registration |
ONEHAZEL_SIGNING_SECRET | all | the signing secret shown at registration |
ONEHAZEL_SIGNING_SECRET_PREVIOUS | all | the old secret, during the one-hour overlap after a rotation |
ONEHAZEL_TEST_PLAYER_ID | wallet, player | the test player id, default test-player-1 |
ONEHAZEL_RESTRICTED_TEST_PLAYER_ID | player | the restricted test player id, default test-player-restricted |
ONEHAZEL_BASE_PATH | TypeScript, Python, plain PHP | changes the default /onehazel/v1 path |
The Laravel samples use routes under /api/onehazel/<module> instead of a base path variable. Each Run it section names the base URL to register.
To check a local build before asking OneHazel to run, sign requests with signForTest (or signWebhookForTest) from @onehazel/connect and replay the scenarios on Conformance. Step 2 of Getting started shows a signed call with curl.
Point conformance at it
OneHazel runs conformance from its own servers, so the module must be reachable on a public https address. A localhost or private address is refused at registration.
- Expose the module on a public https URL: deploy it, or put your local server behind an https tunnel.
- Register that base URL for the module (
connect_register_endpoint, orPOST /operators/me/connect/endpoints). For wallet includetestPlayerIdandtestGame(anyprovider_slug/game_code: the samples accept every game); for player includetestPlayerIdandrestrictedPlayerId, matching the ids the sample seeds. - Restart the sample with the callback key and signing secret from the registration response.
- Start a run (
connect_run_conformance, orPOST /operators/me/connect/conformance) and read the result withconnect_conformance_evidence.
Before you go live
- Replace the in-memory or file stores with your database. The idempotency store must survive restarts.
- Point the
liveledger at your real wallet and keeptesttraffic on the test ledger. - Keep the callback key and signing secret in your secret manager, never in the repository.