Build

Build with Lunium

Pick what you want to build. Each case comes in REST, Node and Python — and with a ready prompt, if you would rather have a coding agent do it.

The prompts are deliberately generic: none names an endpoint, a field or an asset. If they had to, discovery would not really be working.

PIX → USDT

Receive PIX from customers and settle the value into USDT in a wallet.

# 1. test key — one call
curl -X POST https://api.luniumpay.com/keys/sandbox -H 'Content-Type: application/json' \
  -d '{"name":"my-project"}'

# 2. PIX charge; the payer's tax ID is required by the Central Bank
curl -X POST https://api.luniumpay.com/cashin/charge \
  -H "X-API-Key: $LUNIUM_KEY" -H 'Content-Type: application/json' \
  -d '{"amount_cents": 15000,
       "payer_tax": "12345678901",
       "payout_address": "0xYourWallet",
       "chain": "polygon",
       "external_id": "order-123"}'

# 3. follow it until it settles
curl https://api.luniumpay.com/cashin/$ID/status -H "X-API-Key: $LUNIUM_KEY" 
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;

const call = (path, method = "GET", body) =>
  fetch(API + path, {
    method,
    headers: { "Content-Type": "application/json", "X-API-Key": key },
    body: body && JSON.stringify(body),
  }).then((r) => r.json());

// Creates the charge. The amount goes in CENTS — sending 150 instead of 15000
// is the most common mistake, and the API takes both because both are valid.
const charge = await call("/cashin/charge", "POST", {
  amount_cents: 15000,
  payer_tax: "12345678901",
  payout_address: "0xYourWallet",
  chain: "polygon",
  external_id: "pedido-" + Date.now(),
});

if (charge.erro) throw new Error(`${charge.erro}: ${charge.detail}`);
console.log("copia e cola:", charge.qr_copypaste);

// Then: follow it on a signed webhook, or poll the status.
const status = await call(`/cashin/${charge.public_id}/status`);
console.log(status.status, status.settlement_status);
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


charge = call("/cashin/charge", "POST", {
    "amount_cents": 15000,          # cents, not reais
    "payer_tax": "12345678901",     # required by the Central Bank
    "payout_address": "0xYourWallet",
    "chain": "polygon",
    "external_id": "order-123",
})
print("copia e cola:", charge["qr_copypaste"])
print(call(f"/cashin/{charge['public_id']}/status"))
Integrate Lunium starting from https://luniumpay.com. I need to receive PIX and settle into USDT. Discover the available capabilities and API yourself, start in sandbox, and preserve this project's existing architecture.

Hand this to a coding agent. It discovers the API on its own.

USDT → PIX

Sell USDT and have a PIX key receive Brazilian reais.

# 1. quote — commits to nothing, valid for 15 minutes
curl -X POST https://api.luniumpay.com/cash-outs \
  -H "X-API-Key: $LUNIUM_KEY" -H 'Content-Type: application/json' \
  -d '{"asset":"USDT","network":"polygon","brl_amount":"150.00",
       "pix_key":"who-receives@example.com",
       "refund_address":"0xYourUserWallet",
       "external_id":"sale-123"}'

# 2. accept — POINT OF NO RETURN. Returns the deposit address.
curl -X POST https://api.luniumpay.com/cash-outs/$ID/accept -H "X-API-Key: $LUNIUM_KEY"

# 3. follow it; at COMPLETED the Central Bank receipt arrives
curl https://api.luniumpay.com/cash-outs/$ID -H "X-API-Key: $LUNIUM_KEY" 
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;
const call = (p, m = "GET", b) => fetch(API + p, {
  method: m,
  headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: b && JSON.stringify(b),
}).then((r) => r.json());

// 1. quote: send brl_amount OR amount, never both — a quote runs in
// one direction only. Here we say "I want the key to receive R$ 150".
const quote = await call("/cash-outs", "POST", {
  asset: "USDT", network: "polygon", brl_amount: "150.00",
  pix_key: "who-receives@example.com",
  // where the crypto goes back to if the PIX cannot be paid. Without it,
  // it would return to the on-chain origin — an exchange, if that is where the user withdrew from.
  refund_address: "0xYourUserWallet",
  external_id: "venda-" + Date.now(),
});
if (quote.erro) throw new Error(`${quote.erro}: ${quote.detail}`);
console.log(`envie ${quote.amount} USDT → R$ ${quote.brl_amount}`);

// 2. accepting locks the quote and returns the address. After that there is no going back.
const accepted = await call(`/cash-outs/${quote.cashout_id}/accept`, "POST", {});
console.log("deposit em:", accepted.deposit_address);

// 3. at COMPLETED: pix_e2e, receipt_url and verify_url
const done = await call(`/cash-outs/${quote.cashout_id}`);
console.log(done.state, done.pix_e2e ?? "");
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


quote = call("/cash-outs", "POST", {
    "asset": "USDT", "network": "polygon",
    "brl_amount": "150.00",                    # ou "amount", nunca os dois
    "pix_key": "who-receives@example.com",
    "refund_address": "0xYourUserWallet",
    "external_id": "sale-123",
})
print(f"envie {quote['amount']} USDT -> R$ {quote['brl_amount']}")

# point of no return
aceita = call(f"/cash-outs/{quote['cashout_id']}/accept", "POST", {})
print("deposit em:", aceita["deposit_address"])

final = call(f"/cash-outs/{quote['cashout_id']}")
print(final["state"], final.get("pix_e2e", ""))
Integrate Lunium starting from https://luniumpay.com. I need to convert USDT into BRL via PIX. Discover the API yourself, start in sandbox, and preserve this project's existing architecture.

Hand this to a coding agent. It discovers the API on its own.

Payment verification

Confirm a PIX settled before releasing goods, credit or access. No API key needed.

# No key, by design: it lets you check a payment that
# YOU did not make, from a counterparty you have no reason to trust.
curl https://api.luniumpay.com/v1/verificar/E12345678202608291200abcdefghijk
// No credentials. That is the point: anyone can check.
const e2e = "E12345678202608291200abcdefghijk";
const r = await fetch(`https://api.luniumpay.com/v1/verificar/${e2e}`);
const proof = await r.json();

// `nao_encontrado` does NOT mean the PIX never existed — it means
// Lunium did not settle it. Another institution may have.
if (!proof.verificado) {
  console.log("not proven:", proof.erro);
} else {
  // Check the amount and the time yourself: a valid E2E for R$ 1.00
  // is not proof of a R$ 1,000.00 payment.
  console.log(proof.valor_brl, proof.pago_em, proof.instituicao);
}
import json, urllib.error, urllib.request

E2E = "E12345678202608291200abcdefghijk"
url = f"https://api.luniumpay.com/v1/verificar/{E2E}"

try:
    with urllib.request.urlopen(url, timeout=20) as r:
        proof = json.load(r)
except urllib.error.HTTPError as e:
    # 404 is a legitimate answer, not a bug in your code: it means
    # Lunium did not settle this payment — not that it does not exist.
    proof = json.loads(e.read() or b"{}")

if proof.get("verificado"):
    print(proof["valor_brl"], proof["pago_em"], proof["instituicao"])
else:
    print("not proven:", proof.get("erro"))
Integrate Lunium starting from https://luniumpay.com. Build a check that confirms whether a Brazilian PIX payment actually settled, from its end-to-end identifier, before releasing anything. This capability needs no API key — discover it yourself.

Hand this to a coding agent. It discovers the API on its own.

Treasury agent

Move balance between reais and stablecoins on a rule or a schedule.

# The catalog is the source of truth for what settles right now — and it changes on its own.
# Do not hardcode the list.
curl https://api.luniumpay.com/catalog -H "X-API-Key: $LUNIUM_KEY"

# Your limits today (tier, daily ceiling and your mandate, if any)
curl https://api.luniumpay.com/keys/me -H "X-API-Key: $LUNIUM_KEY" 
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;
const call = (p, m = "GET", b) => fetch(API + p, {
  method: m, headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: b && JSON.stringify(b),
}).then((r) => r.json());

// Read the ceiling before sizing the operation — it grows with the
// volume already settled, so hardcoding a number ages badly.
const me = await call("/keys/me");
const disponivel = me.limits?.daily?.available_cents ?? Infinity;

const alvoCents = 500_00;
if (alvoCents > disponivel) {
  console.log("above today's ceiling; split the operation");
} else {
  const quote = await call("/cash-outs", "POST", {
    asset: "USDT", network: "polygon",
    brl_amount: (alvoCents / 100).toFixed(2),
    pix_key: process.env.PIX_TESOURARIA,
    refund_address: process.env.CARTEIRA,
    external_id: "tesouraria-" + new Date().toISOString().slice(0, 10),
  });
  console.log(quote.cashout_id, quote.amount, "USDT");
}
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


# The ceiling grows with settled volume — read it, do not assume.
me = call("/keys/me")
print("tier:", me["limits"]["tier"])

quote = call("/cash-outs", "POST", {
    "asset": "USDT", "network": "polygon", "brl_amount": "500.00",
    "pix_key": os.environ["PIX_TESOURARIA"],
    "refund_address": os.environ["CARTEIRA"],
    "external_id": "treasury-2026-08-29",
})
print(quote["cashout_id"], quote["amount"], "USDT")
Integrate Lunium starting from https://luniumpay.com. Build a treasury agent that moves balance between BRL over PIX and USDT according to a rule. Discover the API yourself and start in sandbox.

Hand this to a coding agent. It discovers the API on its own.

Payment agent

Let autonomous software pay in Brazil, inside a spending policy it sets for itself.

# 1. the agent registers and gets a test credential right away
curl -X POST https://api.luniumpay.com/mesh/agents -H 'Content-Type: application/json' \
  -d '{"name":"my-agent","contact":"me@example.com",
       "capabilities":["usdt_to_pix"]}'

# 2. the policy it imposes on itself. It only TIGHTENS the key limits,
#    never widens them — which is why the agent may set it itself.
curl -X POST https://api.luniumpay.com/mesh/agents/$AGENT_ID/mandate \
  -H "X-API-Key: $LUNIUM_KEY" -H 'Content-Type: application/json' \
  -d '{"max_por_operacao_cents": 30000, "max_diario_cents": 200000}'
const API = "https://api.luniumpay.com";

// 1. registration: returns agent_id and the test credential in one response
const reg = await fetch(`${API}/mesh/agents`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "my-agent", contact: "me@example.com",
    capabilities: ["usdt_to_pix"],
  }),
}).then((r) => r.json());

const { agent_id } = reg.agent;
const key = reg.api_key;   // shown only once

// 2. the agent's own spending policy
await fetch(`${API}/mesh/agents/${agent_id}/mandate`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: JSON.stringify({
    max_por_operacao_cents: 30_000,   // R$ 300 per operation
    max_diario_cents: 200_000,        // R$ 2.000 por dia
  }),
});

// From here on, an order above the ceiling is refused with
// `mandate_valor_acima_do_teto` — by its own policy, not by ours.
import json, urllib.request

API = "https://api.luniumpay.com"


def post(path, body, key=None):
    req = urllib.request.Request(API + path, data=json.dumps(body).encode(), method="POST")
    req.add_header("Content-Type", "application/json")
    if key:
        req.add_header("X-API-Key", key)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


reg = post("/mesh/agents", {
    "name": "my-agent", "contact": "me@example.com",
    "capabilities": ["usdt_to_pix"],
})
agent_id, key = reg["agent"]["agent_id"], reg["api_key"]

# A mandate only TIGHTENS limits — the worst it can do is restrict you.
post(f"/mesh/agents/{agent_id}/mandate", {
    "max_por_operacao_cents": 30000,
    "max_diario_cents": 200000,
}, key=key)
print("agent", agent_id, "with its own policy")
Integrate Lunium starting from https://luniumpay.com. Build an agent that pays a PIX key using stablecoins, with a per-operation and daily limit it configures itself. Discover the API yourself and start in sandbox.

Hand this to a coding agent. It discovers the API on its own.

Wallet with PIX

Give wallet users PIX deposits and withdrawals against USDT or USDC.

# deposit: PIX comes in, USDT goes to the user's wallet
curl -X POST https://api.luniumpay.com/cashin/charge -H "X-API-Key: $LUNIUM_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"amount_cents":10000,"payer_tax":"$USER_CPF",
       "payout_address":"$USER_WALLET","chain":"polygon",
       "external_id":"deposit-$UID"}'

# withdrawal: USDT leaves the wallet, reais land on the user's PIX key
curl -X POST https://api.luniumpay.com/cash-outs -H "X-API-Key: $LUNIUM_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"asset":"USDT","network":"polygon","brl_amount":"100.00",
       "pix_key":"$USER_PIX_KEY","pix_key_type":"cpf",
       "refund_address":"$USER_WALLET","external_id":"withdrawal-$UID"}'
const API = "https://api.luniumpay.com";
const key = process.env.LUNIUM_KEY;
const call = (p, m = "GET", b) => fetch(API + p, {
  method: m, headers: { "Content-Type": "application/json", "X-API-Key": key },
  body: b && JSON.stringify(b),
}).then((r) => r.json());

// DEPOSIT — the user pays a PIX, their wallet receives USDT
export async function deposit(user, centavos) {
  return call("/cashin/charge", "POST", {
    amount_cents: centavos,
    payer_tax: user.cpf,            // required by the Central Bank
    payout_address: user.wallet,
    chain: "polygon",
    external_id: `dep-${user.id}-${Date.now()}`,
  });
}

// WITHDRAWAL — the user sends USDT, their PIX key receives reais
export async function withdraw(user, reais) {
  const q = await call("/cash-outs", "POST", {
    asset: "USDT", network: "polygon", brl_amount: reais.toFixed(2),
    pix_key: user.pixKey,
    // pix_key_type is only required when the key is 11 bare digits:
    // CPF and phone numbers are the same length and cannot be told apart.
    pix_key_type: user.pixKeyType,
    refund_address: user.wallet,
    external_id: `wd-${user.id}-${Date.now()}`,
  });
  return call(`/cash-outs/${q.cashout_id}/accept`, "POST", {});
}
import json, os, urllib.request

API = "https://api.luniumpay.com"
KEY = os.environ["LUNIUM_KEY"]


def call(path, method="GET", body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    req.add_header("X-API-Key", KEY)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


def deposit(user, centavos):
    """PIX comes in, USDT goes to the user's wallet."""
    return call("/cashin/charge", "POST", {
        "amount_cents": centavos,
        "payer_tax": user["cpf"],
        "payout_address": user["carteira"],
        "chain": "polygon",
        "external_id": f"dep-{user['id']}",
    })


def withdraw(user, reais):
    """USDT sai, reais caem na chave PIX dele."""
    q = call("/cash-outs", "POST", {
        "asset": "USDT", "network": "polygon",
        "brl_amount": f"{reais:.2f}",
        "pix_key": user["chave_pix"],
        "pix_key_type": user["tipo_chave"],   # required when they are 11 digits
        "refund_address": user["carteira"],
        "external_id": f"wd-{user['id']}",
    })
    return call(f"/cash-outs/{q['cashout_id']}/accept", "POST", {})
Integrate Lunium starting from https://luniumpay.com. Add PIX deposits and withdrawals to a crypto wallet, settling into USDT. Discover the capabilities and required fields yourself, and start in sandbox.

Hand this to a coding agent. It discovers the API on its own.

They all start the same way

# the whole catalog, no credential
curl https://api.luniumpay.com/mesh

# a test key, one call
curl -X POST https://api.luniumpay.com/keys/sandbox

The sandbox runs the whole flow without moving a cent, and the first two decimals of the amount choose the outcome — you can exercise the failure paths in CI instead of discovering them in production.

How discovery works More examples