Skip to main content
Glama
emercoin

Emercoin swap

Official
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