The api seed (auth.seed_accounts: true, see
deployment.md) inserts a fixed set of test
accounts into Postgres — pn_address, pn_pubkey, the KEK-encrypted
pn_seckey, and pn_dih, from the SEED_DATA literal in
crates/infrastructure/src/seed.rs.
Seeding only writes database rows. It does not create anything on-chain.
Each seeded account points at a Private Note (PN) contract by address; that
contract must already be deployed and funded on the target network. If it is
not, the read paths still work (the rows exist), but the trading path
(POST /order, DELETE /order, batch, buyFullSet) fails the moment the api
submits an external message to a Private Note that does not exist or has no gas.
So the order of operations is:
- Deploy and fund the Private Notes on-chain (this document).
- Record each Private Note's address, keypair, and deposit-identifier hash.
- Put those values into
SEED_DATA(or provision the accounts directly in the database — see auth.md). - Run the api once with
auth.seed_accounts: trueto insert the rows, then turn the flag back off.
A Private Note has to clear three on-chain steps before the trading path can use it:
- Deploy —
RootPN.deployPrivateNote. The Private Note is materialized at a deterministic address derived from its deposit identifier hash (DIH). - SHELL ECC gas —
RootPN.sendEccShellToPrivateNotefunds the Private Note's gas balance in SHELL. - Native top-up — a native vmshell balance so the Private Note can pay for its own internal-message execution. This comes from the giver (see below).
The in-repo CLI
sdk/src/bin/mint_pn_pool.rs runs the full
per-PN flow — halo2 deposit voucher → deployPrivateNote → halo2 SHELL
voucher → sendEccShellToPrivateNote → giver native top-up — and writes the
result to JSON:
cargo run --release --bin mint_pn_pool -- \
--count 10 \
--nominal N10000 \
--token-type nackl \
--endpoint shellnet.ackinacki.org \
--output pn_pool.json| Flag | Default | Meaning |
|---|---|---|
--count / -n |
5 |
number of Private Notes to deploy |
--nominal |
N10000 |
per-PN deposit nominal — N100, N1000, or N10000 |
--token-type / -t |
nackl |
deposit currency — nackl, shell, or usdc |
--endpoint / -e |
shellnet.ackinacki.org |
network host |
--output / -o |
pn_pool.json |
output path; re-running against an existing file appends --count more Private Notes |
Deployment is sequential by design — each halo2 voucher proof commits to current chain state and must be submitted inside its validity window, so Private Notes cannot be minted in parallel.
mint_pn_pool is a maintainer tool with real build- and run-time dependencies:
- Halo2 prover crates, pulled over HTTPS in
sdk/Cargo.tomlfrom public git repos — no SSH key required. - Halo2 artifacts on disk — a writable prover-cache directory, plus the KZG
SRS file
kzg_bn254_19.srs(~64 MB). The SRS is generated automatically on first run if it is missing — it is reproducible from a fixed seed, so the generated file is identical everywhere and matches the on-chain verifier — and cached on disk afterward (the first run pays a one-time CPU cost). Defaults are./params,./params/halo2_cache,./target/halo2_fixtures; override withPARAMS_DIR,HALO2_PK_CACHE,HALO2_FIXTURE_DIR. Seesdk/src/services/halo2/paths.rs. - A working giver on the target network. Shellnet has one; Mainnet does not. On Mainnet, deploying and funding Private Notes requires purchasing SHELL tokens.
- A release build — the halo2 prover is CPU-bound.
If you cannot run this tool (no giver on your network, say), deploy and fund the Private Notes by whatever means your network provides, then record the same four values per Private Note (address, DIH, public key, secret key) by hand.
pn_pool.json holds one entry per Private Note. It contains secret keys — keep
it private and out of any shared store. Each entry carries address,
deposit_identifier_hash, owner_public_key_hex, owner_secret_key_hex, and
the funding flags shell_funded / native_funded. The next section turns these
into seeder input.
The seeder reads one JSON literal, SEED_DATA, compiled into the api binary
(crates/infrastructure/src/seed.rs). To
seed your own Private Notes you edit that literal and rebuild the api image. One
account looks like this:
{
"accounts": [
{
"label": "mm-001",
"pn_address": "0:20e8f9…",
"pn_pubkey_dec": "70969641…",
"pn_seckey_hex": "483ee42a…",
"pn_dih_dec": "41285154…",
"api_keys": [
{
"api_key": "dk_live_001",
"api_secret_hex": "1de6fc5c…",
"permissions": ["USER_DATA", "TRADE"]
}
]
}
]
}Four fields come straight from pn_pool.json (or from however you deployed the
Private Note):
pn_pool.json |
SEED_DATA |
Conversion |
|---|---|---|
address |
pn_address |
none — copy as-is (0:…) |
deposit_identifier_hash |
pn_dih_dec |
none — already decimal |
owner_secret_key_hex |
pn_seckey_hex |
none — already hex |
owner_public_key_hex |
pn_pubkey_dec |
hex → decimal (see below) |
The public key is the one field that needs converting — the pool writes it as
hex, but the seeder's accounts.pn_pubkey column is numeric(78,0), so it wants
the decimal form:
python3 -c "print(int('<owner_public_key_hex>', 16))"label is free-form and optional — it is only there for humans reading the DB.
api_key and api_secret_hex are not produced by the deploy tool — you mint
them yourself, one or more per account:
api_key— the public identifier the client sends in theX-DODEX-APIKEYheader. Any unique string; the baked-in examples use adk_live_…prefix. Uniqueness is enforced per active key on insert.api_secret_hex— the shared secret the client signs requests with (HMAC-SHA256). Generate 32 bytes of hex:It is stored encrypted under the KEK — this literal is the only cleartext copy, so hand it to the client and keep it safe.openssl rand -hex 32
permissions— a non-empty list; each entry is exactlyUSER_DATA(read account and order data) orTRADE(place and cancel orders). Case-sensitive.
SEED_DATA is validated in full before a single row is written — one bad field
makes the api refuse to start (loud on purpose; there is no partial DB state).
The checks:
pn_pubkey_decandpn_dih_dec— decimal non-negative integers that fit in 256 bits.pn_seckey_hexandapi_secret_hex— valid hex.- every
api_keysentry — at least one permission, each a known label.
- In your api config set
auth.seed_accounts: true(see deployment.md for where the flag sits). - Start the api once. It applies migrations, then inserts every account and key
in one transaction, and logs a line like:
seeded credentials accounts_inserted=10 accounts_skipped=0 api_keys_inserted=10 api_keys_skipped=0 - Set
auth.seed_accounts: falseand restart.
Re-running is safe: the insert is idempotent (ON CONFLICT DO NOTHING, keyed on
pn_address for accounts and on the active api_key for keys), so rows that
already exist are counted under *_skipped rather than duplicated or
overwritten. To change an already-seeded account, edit it in the database
directly — re-seeding will not update it.
Editing
SEED_DATAmeans rebuilding. It is a compile-time constant, not a file the running container reads, so changes only take effect in a freshly built api image. If you would rather not rebuild, provision the accounts straight into theaccounts/api_keystables instead — you encryptpn_seckeyandapi_secretunder the KEK yourself; see auth.md for the table contract.
On a network that has a giver (such as Shellnet), the giver tops up a Private
Note directly by its address — the same pn_address that is in pn_pool.json
and in SEED_DATA. mint_pn_pool does this automatically at deploy time, but you
can also top up an already-deployed Private Note later: send SHELL (and native
gas) from the giver to the Private Note's address.
For getting test SHELL and using the giver on Shellnet, follow the Acki Nacki guide:
https://dev.ackinacki.com/readme/get-test-tokens-in-shellnet#get-shell
A Private Note that runs out of gas stops being able to execute trading messages until it is topped up again, so re-funding existing seed Private Notes is the normal way to keep a long-running dev/test environment working.