ODATANOODATANOAPIpreprod
m402

Buy a key with tNIGHT

A key or a top-up is paid on Midnight, through a payment vault. The vault records a receipt hash, not who paid.

How it works

  • 1Ask for the price. POST /keys or POST /topup without a payment answers 402. Each pack has an m402 entry: the price in NIGHT atoms, the vault, the pack's service id.
  • 2Deposit. The vault's deposit(amount, nonceSeed) takes your NIGHT and gives you a shielded credit coin of the same amount.
  • 3Pay the pack. pay(serviceId, credit, receiptSecret) spends a credit coin of exactly the pack's price. receiptSecret is 32 random bytes you keep; the vault stores only its hash.
  • 4Send the secret. Repeat the call with the service id and the secret in PAYMENT-SIGNATURE. The gateway derives the receipt, finds it in the vault and credits the pack once.
http# 1. price
POST https://api.preprod.odatano.dev/keys?pack=500
→ 402  { "x402Version": 2, "accepts": [ …, {
           "scheme": "m402", "network": "midnight:preprod",
           "amount": "5000000", "payTo": "<vault>",
           "extra": { "units": 500, "serviceId": "…" } } ] }

# 4. same call, with the receipt secret
POST https://api.preprod.odatano.dev/keys?pack=500
PAYMENT-SIGNATURE: base64({
  "x402Version": 2, "scheme": "m402", "network": "midnight:preprod",
  "payload": { "serviceId": "…", "receiptSecret": "…" } })
→ 201  { "token": "oda_…", "units": 500, "validUntil": "…" }

Packs

CallsPriceNamed pack
…
  • Network…
  • Vault…
  • Credit colour…
  • Validitya bought key is valid for … from the purchase
  • Price per callevery forwarded call costs 1, metadata and the verify lane are free

Fees, privacy, replay

  • Feeswith a key, the platform sponsor pays the tDUST of deposit and pay; each counts as one Midnight call. Without a key your wallet pays its own fee.
  • Publicdeposits, the pack's price and service, the receipt hash
  • Privatewho paid, and which deposit paid for which pack
  • Replaya receipt is credited once (409 after that); pay refuses a receipt the vault already holds
  • Pendinga receipt the vault does not show yet answers 402 with pending: true; repeat the call once the pay transaction landed
  • With the keyPOST /topup with Authorization: Bearer oda_…, same round trip
  • By key idPOST /topup?key=<id>, no secret needed; the id is on the account page
  • Exact pricedeposit the pack's exact price, so one credit coin pays one pack. redeem(recipient, credit) turns unused credit back into NIGHT.

Client

  • Packages@odatano/nightgate-tx builds and proves the vault calls on your machine; @odatano/contract-m402-vault is the vault contract
  • Walleta 64-byte seed with NIGHT on the network above; the seed never leaves your process
  • Syncdeposit needs the wallet's NIGHT coins (walletSync: 'coins'), pay the shielded coins ('shielded'); with a key the wallet syncs through the gateway
  • Contractgithub.com/ODATANO/NIGHTGATE-CONTRACTS, package packages/m402-vault
jsimport { randomBytes } from 'node:crypto';
import { createTxBuilder } from '@odatano/nightgate-tx/txbuilder';
import { connect } from '@odatano/nightgate-tx/client';

const api = { baseUrl: 'https://api.preprod.odatano.dev', token: process.env.ODATANO_ACCESS_KEY };
const topup = (h = {}) => fetch(`${api.baseUrl}/topup?pack=500`, { method: 'POST', headers: { Authorization: `Bearer ${api.token}`, ...h } });
const offer = (await (await topup()).json()).accepts.find((a) => a.scheme === 'm402');

// one sponsored vault call; walletSync 'coins' for deposit, 'shielded' for pay
async function call(circuitId, args, walletSync) {
  const b = await createTxBuilder({ seedHex, networkId: 'preprod', api, package: '@odatano/contract-m402-vault', walletSync });
  await b.waitForSync();
  const { finalizedTxB64 } = await b.buildSponsorable({ contractAddress: offer.payTo, call: { circuitId, args, witnesses: {} } });
  await b.close();
  return connect(api).sponsorFinalized({ finalizedTxB64 });
}

const price = BigInt(offer.amount), serviceId = offer.extra.serviceId, secret = randomBytes(32);
await call('deposit', [price, randomBytes(32)], 'coins');
await call('pay', [Buffer.from(serviceId, 'hex'), { nonce: randomBytes(32), color: Buffer.from(offer.extra.creditColor, 'hex'), value: price }, secret], 'shielded');

const payment = { x402Version: 2, scheme: 'm402', network: offer.network, payload: { serviceId, receiptSecret: secret.toString('hex') } };
const res = await topup({ 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(payment)).toString('base64') });