Demo mode: a reference storefront for eSIM operators. Prices are live, payments are off. Run your own operator →
Travel eSIM · demo
English

Operator guide

Contents

This guide takes you from an empty server to your first sale, then covers the daily work: orders, refunds, balance, backups, updates. Plan an hour for the setup. Most of that hour is waiting for the wholesale account and the DNS record.

Other documents: overview · agent guide

1. What you are setting up

            https://esim.example.com
                      │
                   Caddy (80, 443, Let's Encrypt)
      ┌───────────────┼──────────────────────────────┐
      │               │                              │
   store          rails (/agents/*)              node (/.well-known/ai-market.json, /ai-market/*)
 people buy      agents buy over x402            hubs find your offers
      │               │
      └───── your eSIM Access balance ─────┘   payments go to your own wallets

Both store and rails buy from the same wholesale account. Neither one holds money: buyers pay to your addresses, and the kit only reads the networks to see the payments arrive.

2. Before you start

You needNotes
An eSIM Access accountSign up at esimaccess.com. Step 3.
$50 or more on its balanceThe first top-up is at least $50 (card, PayPal or Alipay; bank transfer from $3,000).
A Linux serverDocker Engine with the compose plugin, a public IPv4 address, ports 80 and 443 open. 1 vCPU and 1 GB of memory are enough.
A domain or subdomainFor example esim.example.com, with an A record pointing at the server.
Wallet addressesEVM (one address for all seven EVM networks), Tron, Solana, TON. Any subset will do.
Terms and a privacy policyTwo public pages. eSIM Access expects every reseller's site to link them.
Optional: a Coinbase Developer Platform keyEnables gasless USDC on Base for people, and agents' x402 orders. Step 8.
Optional: StripeEnables cards. Step 8.

3. The wholesale account

  1. Sign up at esimaccess.com and confirm your email.
  2. Top up the balance (in the console: balance, then top up). The minimum is $50. Every eSIM you sell is paid from this balance at the wholesale price, so it is your stock, not a fee.
  3. Open the developer (API) section of the console. It shows two values: the AccessCode and the SecretKey. Treat the SecretKey like a password.

There is no sandbox: every API order is real. Two ways to test cheaply:

4. The server and the domain

  1. Create an A record for your domain pointing at the server's IPv4 address. Check it from your computer:
dig +short esim.example.com
  1. Install Docker on the server (docs.docker.com/engine/install) and check that compose works:
docker compose version
  1. Open ports 80 and 443 in the server's firewall and in your hosting provider's panel. Let's Encrypt checks port 80 before it issues the certificate.

5. Get the kit and configure it

git clone https://github.com/alexar76/esim-operator-kit.git
cd esim-operator-kit
cp .env.example .env
nano .env
SettingWhat to put
DOMAINYour domain, without https://.
STORE_NAMEThe name buyers see in the header, the title and wallet prompts.
PRODUCT_IDA short slug for your offers in the federation, for example esim-mytravel. Letters, digits, dashes.
STORE_CONTACTThe email buyers write to. It is shown in the footer.
STORE_TERMS_URL, STORE_PRIVACY_URLYour terms and privacy policy. They are linked in the footer.
MARGIN_PCTYour markup on the wholesale price, in percent. 100 doubles it.
MIN_MARGIN_USDThe smallest margin per eSIM, in dollars. Cheap plans get at least this much on top.
DEMO_MODE1 shows a banner and takes no payment. 0 sells. Keep 1 until step 10.
PAY_TO_EVM, PAY_TO_TON, PAY_TO_TRON, PAY_TO_SOLANAYour receiving addresses. Leave a line empty to turn that network off.
EVM_CHAINSWhich EVM networks to offer, as chain ids: 8453 Base, 1 Ethereum, 42161 Arbitrum, 10 OP, 137 Polygon, 56 BNB Chain, 43114 Avalanche.
EVM_RPC_<chain id>, SOLANA_RPCOptional: your own RPC endpoints. The public ones are rate limited.

How the price is made. Price = wholesale × (1 + MARGIN_PCT/100), but never less than wholesale + MIN_MARGIN_USD. It is rounded up to the cent. With the defaults (100 % and $1.00), France 1 GB for 7 days costs $0.70 wholesale and sells for $1.70. A 20 GB plan at $9.00 wholesale sells for $18.00. Retail eSIM shops typically ask $4 to $8 for the small plans, so you have room.

Addresses only. Every PAY_TO_* is an address you receive at. The kit never needs a private key or a seed phrase. If a guide or a person asks you to put one in .env, it is wrong.

6. Put the keys in place

The keys live in secrets/, one value per file. The folder is git-ignored and mounted read-only into the containers. Type the values in without them landing in your shell history:

cd ~/esim-operator-kit
umask 077
printf 'AccessCode: '; read -rs v; printf %s "$v" > secrets/esimaccess_access_code; echo
printf 'SecretKey: '; read -rs v; printf %s "$v" > secrets/esimaccess_secret_key; echo
unset v

The containers run as user 1000, so give that user the key files and the data folders:

mkdir -p data/store data/rails data/node data/caddy
sudo chown 1000:1000 secrets/*
sudo chown -R 1000:1000 data/store data/rails data/node
ls -l secrets

The folder itself stays yours, so you can add files later. Each new key file needs the same sudo chown 1000:1000 and chmod 600.

secrets/README.md lists every optional file: the CDP key, the tonapi and TronGrid keys, and the Stripe keys.

7. Start in demo mode

docker compose up -d --build
docker compose ps
docker compose logs --tail 30 store rails

The first build takes a few minutes. Then check three things:

curl -s https://esim.example.com/agents/healthz
curl -s -X POST https://esim.example.com/agents/quote -H 'content-type: application/json' \
  -d '{"country":"FR","min_gb":3,"min_days":7,"limit":3}'

If the health answer says "ok":false, the wholesale keys are wrong or unreadable. See section 14.

Check your prices. This prints your balance and the top offers for any countries, at your margin, wholesale price beside each:

docker compose exec -T rails node scripts/catalog.mjs FR ES IT US JP TH

Change MARGIN_PCT or MIN_MARGIN_USD in .env and apply it with docker compose up -d.

8. Payments

Each method is on once its address (and key, where one is needed) is set. Turn on the ones your buyers use. USDT on Tron is the most common way to pay in crypto; USDT and USDC on EVM networks and USDT on TON follow.

MethodSetHow the buyer paysHow the store confirms it
USDC on Base, gaslessPAY_TO_EVM + secrets/cdp_api_key.jsonSigns one message in a browser wallet and pays no gas.The CDP facilitator settles the signature. The eSIM is delivered first, then the payment settles; if settlement fails, the eSIM is cancelled.
USDC / USDT on 7 EVM networksPAY_TO_EVM (+ EVM_CHAINS)Sends the transfer from a browser wallet, or from any wallet or exchange and pastes the transaction hash.Reads the receipt: a transfer of that token to you, of at least the price, newer than the order. It waits for confirmations: 3 on Ethereum and BNB Chain, 10 on Polygon, 1 elsewhere. One transaction pays one order.
USDT on TronPAY_TO_TRON (+ secrets/trongrid_key)Sends the exact amount shown, from TronLink on the page or from any wallet.Each order asks for the price plus a few millionths of a dollar (at most $0.0099 more). That exact amount identifies the order among your confirmed USDT transfers.
USDC on SolanaPAY_TO_SOLANA (+ SOLANA_RPC)Opens the Solana Pay link or scans its QR code (Phantom, Solflare, Backpack).Each order has its own reference key, which the wallet adds to the transfer. The store finds the transaction by it.
USDT or TON on TONPAY_TO_TON (+ secrets/tonapi_key)Connects Tonkeeper or another wallet through TON Connect, or pays by hand with the comment shown.Finds the transfer by the order's comment through tonapi.io.
Cardssecrets/stripe_secret_key + secrets/stripe_webhook_secretStripe Checkout.The Stripe webhook.

A pushed payment (every method except gasless Base and cards) is looked for during 24 hours after the order is made.

Coinbase Developer Platform key. It enables gasless USDC on Base for people and the x402 orders from agents. Without it, the store still takes Base USDC as a plain transfer, but the agent node takes no orders.

  1. Sign in at portal.cdp.coinbase.com and create a project.
  2. API Keys: create a secret API key and download its JSON file.
  3. Copy the file to secrets/cdp_api_key.json (it holds id and privateKey), then run sudo chown 1000:1000 secrets/cdp_api_key.json && sudo chmod 600 secrets/cdp_api_key.json and docker compose up -d.

Stripe. In the Stripe dashboard, add a webhook endpoint https://<your domain>/api/stripe/webhook with the events checkout.session.completed and checkout.session.async_payment_succeeded. Put the secret key (sk_live_…) in secrets/stripe_secret_key and the endpoint's signing secret (whsec_…) in secrets/stripe_webhook_secret.

Rate limits. The public TronGrid, tonapi and RPC endpoints work for a small store. Once you have regular sales, add a free TronGrid key and a tonapi key, and point EVM_RPC_<id> and SOLANA_RPC at your own provider.

9. Agents and the federation

The rails node already answers agents at https://<your domain>/agents/. To let agents find you by searching a hub, describe your node to the federation and announce it.

  1. Write node.toml from your live catalogue. Use your own name, domain and PRODUCT_ID:
docker compose exec -T rails node scripts/node-toml.mjs \
  --name "My Travel eSIM" --url https://esim.example.com \
  --product esim-mytravel --upstream http://rails:9490 > node.toml
head -20 node.toml
  1. Start the node:
docker compose --profile federation up -d --build node
curl -s https://esim.example.com/.well-known/ai-market.json | head -c 400; echo
  1. Check it the way a hub will, then announce it:
docker compose exec node aimarket doctor --url https://esim.example.com
docker compose exec node aimarket node knock --config /node/node.toml --hub https://modelmarket.dev

The hub calls esim.quote with a real country before it lists you. Once listed, an agent that searches the hub for "eSIM France" gets your offers. Payment never goes through the hub: the agent pays your node over x402, straight to your wallet.

data/node/ holds the node's signing key, which is its identity in the federation. Back it up. Rerun step 1 when the catalogue gains countries.

10. The test order, then go live

  1. In .env, set DEMO_MODE=0 and check the PAY_TO_* addresses once more, character by character.
  2. Apply it:
docker compose up -d
curl -s https://esim.example.com/agents/healthz

"demo":false should show. "paid_orders":true shows when the CDP key and PAY_TO_EVM are set.

  1. Buy the cheapest plan for a country on your own store, with your own wallet, on the network you expect buyers to use most. The order page should show the QR code within a minute.
  2. Check the payment in your wallet, and the order in the eSIM Access console.
  3. If you do not want to keep the eSIM, cancel the unused profile in the eSIM Access console. The wholesale price returns to your balance. The payment you made went to your own wallet anyway.

Do the same for each network you turned on. Then you are live.

11. Daily work

Orders. The store keeps them in data/store/orders.json. List the ones that need you:

sudo jq -r '.[] | select(.status=="refund_due" or .status=="underpaid")
  | [.created_at, .id, .status, .method, (.price_cents/100), .payment.ref, (.payment.payer // "-"), (.error // "")] | @tsv' \
  data/store/orders.json
StatusMeaningWhat to do
awaiting_paymentThe order is made and nothing has arrived yet.Nothing.
deliveredPaid, and the eSIM is with the buyer.Nothing.
refund_duePaid, but the wholesaler did not deliver: the balance ran out, or the plan was withdrawn.Refund the buyer to payment.payer on the same network, or top up and sell them the plan again by hand.
underpaidA payment arrived below the price.Refund it, or ask the buyer to send the difference and deliver by hand.
expiredNothing arrived within 24 hours.Nothing. A payment that arrives later is not matched; you will see it in your wallet with no order.

Buyers write to STORE_CONTACT. Ask them for the order number from their page address (/o/<number>). Do not ask for the full link: it contains their token.

Balance. Keep the wholesale balance above a few days of sales. When it cannot cover an order, the checkout says the plan is "temporarily unavailable" instead of taking the money. Check it with:

docker compose exec -T rails node scripts/catalog.mjs | head -1

Logs.

docker compose logs -f --tail 100 store rails

The logs never contain an activation code or a key.

Backups. Back up data/ and secrets/ daily, somewhere other than the server:

Updates.

git pull
docker compose up -d --build
docker compose --profile federation up -d --build node    # if you run the federation node

Read the release notes before you update a store that sells.

12. Agents' orders

Agents buy through POST /agents/order (see the agent guide). The node verifies the x402 payment, buys the profile, and settles only after the profile exists. If settlement fails, it cancels the profile, and the wholesale price returns to your balance. Agents' orders are recorded in data/rails/orders.json, without activation codes. The USDC arrives in PAY_TO_EVM on Base.

13. Law, taxes and terms

This is not legal or tax advice. Ask an accountant in your country before you sell.

14. Troubleshooting

What you seeCauseFix
The browser says the certificate is invalidDNS does not point here yet, or port 80 is closeddig +short <domain>, open port 80, then docker compose restart caddy and read docker compose logs caddy.
healthz shows "ok":false or the store shows no countriesThe wholesale keys are wrong or user 1000 cannot read them. The logs say catalogue not loaded yet: <code>.sudo rm secrets/esimaccess_*, write them again as in step 6, then docker compose restart store rails.
EACCES or permission denied in the logsThe data folders are not owned by user 1000sudo chown -R 1000:1000 data/store data/rails data/node
The checkout says the plan is "temporarily unavailable"The wholesale balance does not cover the planTop up eSIM Access.
Every payment answers "demo mode"DEMO_MODE=1Step 10.
A network is missing at checkoutIts PAY_TO_* is emptyCheck .env, then docker compose up -d.
The store restarts again and againA PAY_TO_* is not a valid address: the store refuses to start with onedocker compose logs store names it. Fix .env.
A Tron payment is not foundThe amount was not exact, often because an exchange deducted its fee from itFind the transfer in your wallet and refund it, or deliver by hand. Tell buyers to send the exact amount from a wallet.
A TON payment is not foundThe comment was missing or changedSame as above. With TON Connect the wallet fills the comment in.
aimarket doctor reports a private addressurl in node.toml is not your public domainWrite node.toml again (step 9) with --url https://<domain>.
node does not start: node.toml is a directoryCompose created it because the file did not existrmdir node.toml, write it (step 9), start again.

The kit on GitHub →