Guides · Part 7 of 7
Run your own gateway
Verifying a Label 309 record needs nothing from anyone — no account, no server, no key. Publishing is the other half: it puts a transaction on Cardano, and that costs a fee. The gateway is the piece that pays it. It holds the funded Cardano wallet that anchors the record (and an Arweave wallet for sealed files), builds and submits the transaction, tracks it to confirmation, and serves the records back for standalone verification.
Most of the time you point your SDK or CLI at someone else's gateway. But the
whole backend is open source — label-309-gateway,
a single Rust binary plus Postgres — so you can run your own. Run one privately
for your own apps, or operate one as a paid service for others: it is
multi-tenant by design, with many accounts and API keys under one instance. This
guide takes you from nothing to a first priced quote using the reference Docker
Compose.
Before you start
You need Docker and Docker Compose, and a host with a few gigabytes of free disk. Everything below targets Cardano preprod — the safe posture for bringing a deployment up. Moving to mainnet is a deliberate switch you make later, once the deployment is funded and verified.
Get the code and write the config
Clone the repository and work from deploy/, where the reference
docker-compose.yml lives. Copy the example config and edit it for your
deployment — network, fee band, storage, and pricing:
git clone https://github.com/cardanowall/label-309-gateway
cd label-309-gateway/deploy
cp ../gateway.example.toml gateway.tomlThe example is annotated and already targets preprod, so it boots as-is for a first run.
Create the keyring
Every signing key the gateway uses lives in one age-encrypted keyring file.
Create it, then add a Cardano key (preprod), an Arweave key, and a webhook-wrap
key. These keyring steps run the gateway binary directly — the keyring is
meant to be created on a trusted machine and only its encrypted file copied into
the container, so grab a release build from the repository for this one step.
mkdir -p secrets
printf '%s' 'a-strong-passphrase' > secrets/gateway-keyring-passphrase
export GATEWAY_KEYRING_PASSPHRASE="$(cat secrets/gateway-keyring-passphrase)"
gateway keyring init --path secrets/gateway-keyring.age
gateway keyring add-cardano --path secrets/gateway-keyring.age --network preprod
gateway keyring add-arweave --path secrets/gateway-keyring.age
gateway keyring add-webhook-wrap --path secrets/gateway-keyring.age
chmod 600 secrets/*Each add- step prints the address it just created. Fund the two printed
addresses: the Cardano address pays anchoring fees, and the Arweave address
funds storage credits for sealed files. Then set the database password the
compose reads:
echo 'POSTGRES_PASSWORD=a-strong-db-password' > .envBring it up
Provision the operator first. This applies the migrations and prints the root secret exactly once — store it somewhere safe, it is the master control-plane credential:
docker compose run --rm gateway operator bootstrap --label acmeThen start Postgres and the gateway:
docker compose up -dRegister a wallet, an account, and a key
The compose publishes no host port on purpose: the data and control planes share one socket, so exposing it would expose the control plane too. Drive the control plane from inside the container. Open a shell:
docker compose exec gateway shPoint it at the local control plane with the root secret bootstrap printed,
then register the funded Cardano wallet (the address keyring add-cardano
printed), create an account, credit it, and mint a data-plane API key scoped for
publishing:
export GATEWAY_CONTROL_URL=http://127.0.0.1:8080/control/v1
export GATEWAY_CONTROL_TOKEN='ctl_…the root secret from bootstrap…'
gateway admin wallet register primary addr_test1… preprod
gateway admin account create
gateway admin account fund <account_id> 10000000 "starter credit" # $10
gateway admin key create <account_id> poe:create,poe:read,account:readkey create prints the API key that drives the data plane. That, plus the base
URL, is everything an app needs.
Take your first quote
To reach the data plane from the host during setup, uncomment the loopback
ports mapping (127.0.0.1:8080:8080) in docker-compose.yml and run
docker compose up -d again — loopback only, never 0.0.0.0. Then lock a price
with the API key you just minted:
curl -sS -X POST http://127.0.0.1:8080/api/v1/poe/quote \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"record_bytes":256,"recipient_count":0,"file_bytes_total":0}'The response carries a quote_id valid for 15 minutes plus a cost breakdown. A
publish call consumes that quote_id atomically with the balance debit — the
same two-step lock a price, then submit shape the SDK guides use. Your
gateway is live.
Where to go next
The running binary serves its own interactive API reference — the data plane at
/api/v1/docs and the control plane at /control/v1/docs, each rendered fully
offline. Two documents in the repository go deeper: the
operator runbook
covers funding, credentials, money semantics, and day-2 operations, and the
integrator guide
covers building a product on top.
Your gateway's data plane is exactly what the SDK and CLI guides mean by a base
URL. Swap https://your-gateway.example for the address you now control, hand
out API keys, and publish your first PoE
through a backend that is entirely yours.
Preprod first, mainnet when you're ready
Everything here targets Cardano preprod — the safe way to bring a deployment up. Moving to mainnet
is a deliberate switch: set network = "mainnet" in the config, add a mainnet Cardano key to the
keyring, and register the wallet as mainnet. All three have to agree before the gateway will
anchor a real transaction.