Emercoin swap
Officialby emercoin
README.md
# swap — EMC cashier (USDT → EMC)
A minimal cashier exposing one primitive. All business logic stays in the
calling services; swap knows nothing about NVS/DNS/subscriptions.
```
buy_emc(amount_usdt, destination_emc_address, callback_url, ref)
→ collect USDT on a unique deposit address
→ on confirmation, deliver EMC (fixed rate ×10) to destination
→ notify the caller with a SIGNED callback
```
`destination` is opaque to swap: it can be a **service's** address (the service
then renders its own product on that EMC — the user only ever pays USDT and
never touches a wallet) or the **user's own** address (raw on-ramp).
## Use via MCP
An AI agent with USDT can buy EMC directly — **no account, no API key, no
callback**. swap exposes a keyless MCP exchanger over **Streamable HTTP** at:
```
https://swap.emercoin.com/mcp
```
Add it to a client:
```bash
# Claude Code
claude mcp add --transport http swap https://swap.emercoin.com/mcp
```
- **Claude Desktop** — Settings → Connectors → *Add custom connector* → the URL above.
- **MCP Inspector** — `npx @modelcontextprotocol/inspector` → Streamable HTTP → the URL.
Tools:
| Tool | What it does |
|------|--------------|
| `get_swap_config` | min/max USDT per order + the fixed EMC-per-USDT rate |
| `buy_emc` | open an order → returns a deposit address + the **exact** USDT amount to send (+ optional `idempotency_key`) |
| `get_order_status` | poll an order by its token until delivered |
| `cancel_order` | drop an unpaid order early |
Flow: `get_swap_config` → `buy_emc(amount, your_emc_address)` → send the **exact**
returned amount (TRC20) to the deposit address → poll `get_order_status` by token
until `notified`. One-way, exact-amount, **no refunds**. The same primitive is also
available as keyed **REST** (for services, with a signed callback) and a keyless
**web** page at [swap.emercoin.com](https://swap.emercoin.com).
> **This server is not self-contained — use the hosted endpoint above.** Running an
> order needs deployed infrastructure: an Emercoin node + [adapter](#layout) (the EMC
> payout rail, `/wallet/send`), a TronGrid USDT watcher, a funded EMC reserve, and a
> configured TRON deposit address. An image built from this repo *in isolation* — e.g.
> by a registry/sandbox like **Glama** — is therefore an **introspection target only**:
> `tools/list` (for tool-definition quality scoring) works with zero config, but
> **execution tools like `buy_emc` cannot complete** without that backing infra (no
> deposit address → "deposit address not configured"; no adapter → reserve pre-flight
> fails). That's correct isolation, not a defect. To actually buy EMC, call the hosted
> `https://swap.emercoin.com/mcp`. The repo never ships the adapter key or any wallet
> secret — those live only on the deployed host.
## Locked decisions
| Topic | Decision |
|-------|----------|
| Rate | static **1 USDT = 10 EMC** |
| Amounts | **fixed denominations: 5 or 10 USDT** (not a free range; floor 5: below it TRON gas dominates). REST/MCP/web validate the input against this exact set |
| USDT rail | **TRC20 (TRON)** |
| Payment match | **one shared deposit address + unique per-order amount tag** |
| EMC delivery | via **emercoin adapter** `POST /wallet/send` (`X-Internal-Key`) |
| Callback signature | **HMAC-SHA256** over canonical body, per-service secret |
| KYC | none (amounts far below threshold) |
| AML | minimal but mandatory — OFAC SDN + Tether freeze blacklist |
| Terms | exact amount, single transfer, **one-way (no refunds)** — state in the offer |
## Layout
```
swap/
config.py env-driven settings (pydantic-settings)
models.py OrderStatus enum + request/response schemas
states.py order state machine (allowed transitions)
schema.sql DDL: services/orders/deposits/aml_checks/sweeps/callbacks
db.py SQLite connection + init
repository.py DB access layer
auth.py caller auth by API key
orders.py buy_emc business logic (shared by REST + MCP)
main.py FastAPI app (REST: POST /buy_emc, GET /order/{id})
mcp_app.py keyless MCP exchanger at /mcp (agent tools, mirrors /web)
web.py public keyless /web/* channel (raw on-ramp for humans)
site/ static exchanger page + offer (index.html, oferta.html)
clients/
adapter.py EMC delivery + balance via emercoin adapter
trongrid.py TRC20 deposit watcher source (TronGrid)
tron/
hd.py HD derivation of deposit addresses (BIP44, coin 195)
services/
aml.py OFAC + Tether blacklist screening
delivery.py deliver EMC from reserve (idempotent)
callback.py signed callback notifier + retries
watcher.py background loop: deposits → confirm → AML → deliver → notify
sweep.py USDT consolidation from deposit addresses
```
## State machine
```
created → awaiting_payment → confirmed → emc_delivered → notified (done)
↘ underpaid (top-up or partial refund)
↘ overpaid (refund excess)
↘ aml_hold (sender blacklisted → manual)
↘ deliver_failed (retry; else refund USDT)
expired — no payment before TTL
```
## Dev
```bash
uv sync --extra dev
cp .env.example .env # fill secrets
uv run uvicorn swap.main:app --reload --port 8002
```
EMC delivery and the TRON watcher need the emercoin adapter and TronGrid creds;
for local end-to-end you can bring up the node+adapter from `emercoin_docker`
(`docker compose --profile dev up`). TRON parts are verified in a test
environment before they are wired into the watcher.
> **Schema changes have no migrations.** `db.py` applies `schema.sql` with
> `CREATE TABLE IF NOT EXISTS`, which does **not** alter an existing table. After
> editing `schema.sql` in dev, reset the database: `rm swap.db` (then restart —
> it recreates the schema — and re-run `scripts/register_service` since the
> `services` table is wiped too). `swap.db` holds only local/test data.
> Status: **full happy path verified live end-to-end** (+ 41 unit tests). On
> 2026-06-14 a real run took a TRC20 USDT deposit on TRON **Nile testnet** →
> `confirmed` → delivered real **EMC on Emercoin mainnet** (via the adapter
> `/wallet/send`) → **signed callback** verified by the receiver against the
> service's HMAC secret:
> `awaiting_payment → confirmed → emc_delivered → notified`.
> See `docs/TESTNET.md` for the runbook (`scripts/testnet/`).
>
> AML is live: OFAC SDN addresses (TRON) loaded into memory + refreshed, and a
> per-deposit live Tether `isBlackListed` check; a hit → `aml_hold` (no delivery).
>
> Payment matching: pivoted from a unique HD address per order to **one shared
> deposit address + a unique per-order amount tag** (matched by exact amount).
> This removes per-order sweeping and the fresh-address gas penalty; the trade-off
> is exact-amount, single-transfer payments (no auto under/overpaid). Collected
> USDT is moved to treasury / off-ramp manually at low volume.
> MCP exchanger: the keyless web on-ramp is also exposed as MCP tools (`buy_emc`,
> `get_order_status`, `cancel_order`, `get_swap_config`) over Streamable HTTP at `/mcp`, so
> an AI agent buys EMC with USDT directly — no key, no callback, same anti-spam as
> the web channel. No auth by design: it's a public "pay for a service" on-ramp,
> not an account. Tool definitions pass a pre-publication TDQS review (all tier A);
> see `docs/TDQS.md`.
>
> Deferred: USDT sweep / TRON tx signing (`services/sweep.py`, kept off the hot
> path).
## License
MIT — see [LICENSE](LICENSE).
TDQS
A4.6/5.0
Scored across 4 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: initiating an order, canceling unpaid orders, checking status, and retrieving configuration. No functional overlap.
Naming Consistency5/5
All tool names follow a consistent verb_noun snake_case pattern (buy_emc, cancel_order, get_order_status, get_swap_config), making it easy to predict functionality.
Tool Count5/5
With 4 tools covering the core swap workflow, the count is well-scoped and each tool earns its place without excess or deficiency.
Completeness4/5
The set covers order creation, cancellation, status polling, and configuration retrieval. Minor gap: no tool for listing a user's full order history, but the core lifecycle is complete.
Maintenance
ActivityInactive
ResponsivenessNo issues