Guide
Everything you need to integrate a Turkish TRY ⇄ USDC ramp against this sandbox — and to understand what a production anchor in Türkiye actually does behind the same API shape.
Overview
TR Mock Anchor is a sandbox anchor: a service that moves value between Turkish lira in the banking system and USDC on Stellar. It has one door: the SEP path. It implements SEP-1, SEP-10, SEP-6, SEP-12 and SEP-38, so any Stellar wallet or SDK can discover the anchor from its stellar.toml and ramp without any custom code. Integrate it once here and the same code works against any real SEP anchor by changing only the network and the home domain.
The model mirrors a Turkish ramp:
- Your user logs in with their Stellar key (SEP-10). KYC is simulated (SEP-12) and needs no personal data.
- The user starts a deposit and gets bank details: the anchor's IBAN and a personal reference code to write in the description.
- The TRY transfer arrives with that reference → the user has a TRY balance.
- It converts at the USD/TRY rate and real testnet USDC is paid on Stellar to their wallet.
- Off-ramp is the reverse: send USDC with a memo → TRY → payout to the IBAN.
Türkçe özet
Bu servis, Türkiye'deki borsaların çalıştığı şekilde bir TL ⇄ USDC ramp'ini Stellar testnet üzerinde taklit eder. Tek kapı standart SEP yoludur (SEP-1/10/6/12/38). Kullanıcı kendi Stellar anahtarıyla giriş yapar (SEP-10); KYC simüledir ve kişisel veri istemez (SEP-12). Kullanıcı bir yatırma başlatır ve kendisine özel bir referans kodu ile anchor'ın IBAN'ını alır. Gerçek hayatta kullanıcı bu kodu banka transferinin açıklamasına yazar; sandbox'ta transferi POST /sep6/tx/{id}/simulate-bank-transfer ile siz tetiklersiniz. TL bakiyesi oluşur, kur (Reflector USD/TRY oracle + spread) bir SEP-38 quote ile kilitlenir ve gerçek testnet USDC kullanıcının Stellar adresine gönderilir (trustline yoksa claimable balance). Offramp'te kullanıcı memo ile USDC gönderir; anchor ödemeyi tespit eder, TL'ye çevirir ve IBAN'a ödemeyi simüle eder. Banka, KYC ve ödeme tarafı sahte; Stellar tarafı gerçektir.
What is real, what is mocked
| Piece | Real? | Notes |
|---|---|---|
| Stellar payments to wallets (on-ramp) | Real | Testnet USDC from the treasury; payment or claimable balance |
| Detecting USDC deposits (off-ramp) | Real | Horizon payments stream, matched by memo id / muxed id |
| Rates | Real (indicative) | Reflector USD/TRY oracle on Stellar mainnet, plus a flat spread |
| Balances, ledger, quotes | Real logic | Same behaviour a production system would have |
| Incoming TRY bank transfers | Simulated | You call the sandbox endpoint that a bank integration would call |
| TRY payouts to IBANs | Simulated | Instant record with a FAST-style reference |
| KYC / compliance | Simulated | SEP-12: NEEDS_INFO until any PUT, then ACCEPTED; no personal data required, identity numbers never stored |
| SEP-10 auth, SEP-6/38 protocol surface | Real | Verified with SDF's anchor-tests; usable from the Stellar demo wallet |
One door: SEP
This anchor has one door: the standard, portable SEP path. Any Stellar wallet or SDK discovers it from stellar.toml and ramps with no custom code, because SEP-1/10/6/12/38 are the same on every anchor. Code you write against this mock moves to any real SEP anchor by changing only the network and the home domain.
tr-mock-anchor.fly.dev.Concepts
Customers & KYC
A user is identified by their Stellar account, established at SEP-10 login — there is no signup and no personal data. KYC is SEP-12 simulated: a new user is NEEDS_INFO with only optional fields, and any PUT /sep12/customer — even an empty object — flips them to ACCEPTED. If the wallet sends a Turkish IBAN as bank_account_number it is validated (mod-97) and used for payouts; otherwise a deterministic sandbox IBAN stands in. Identity numbers, birth dates and documents are discarded on receipt, never stored. SEP-6 deposit and withdraw do not require the SEP-12 step — authenticating is enough. Full detail in SEP-12 below.
Turkish rails: IBAN, FAST/EFT/Havale, açıklama
Retail money in Türkiye moves between bank accounts identified by a 26-character IBAN (TR + 2 check digits + 5-digit bank code + 1 reserved digit + 16-digit account). Transfers run on three rails: FAST (instant, 24/7, the default for retail amounts), EFT (interbank, business hours) and Havale (same bank). Every transfer carries a free-text description — the açıklama.
Exchanges credit deposits by matching that description: each user gets a personal reference code and is told to write it in the açıklama when sending money to the exchange's IBAN. Transfers without a recognisable reference are held and matched manually by support. This sandbox reproduces the path through SEP-6:
- A SEP-6
depositreturns SEP-9instructions: the bank name, IBAN (bank_account_number), account holder and the user's reference (external_transfer_memo) to write in the açıklama — show this in your app. - When the TRY transfer arrives with that reference the deposit is credited. In this sandbox you trigger it with
POST /sep6/tx/{id}/simulate-bank-transfer {"amount":"…"}(or the transaction'smore_info_urlbutton). In production the real bank transfer does this; unmatched transfers are the support-desk path.
Balances & ledger
Each user has a TRY balance (2 decimals) and a USDC balance (7 decimals, Stellar's precision). All amounts are decimal strings; the service does fixed-point integer math internally and never rounds up in the user's favour. Every movement is an append-only ledger entry with a kind:
| kind | currency | when |
|---|---|---|
bank_transfer | TRY + | a TRY transfer was matched to the customer |
onramp | TRY − | on-ramp created (debited up front) |
onramp_refund | TRY + | on-ramp failed permanently |
offramp_deposit | USDC + | USDC arrived for an off-ramp |
offramp_convert | USDC − / TRY + | the deposit was sold for TRY |
payout | TRY − | TRY sent to the IBAN |
Rates, quotes & spread
The anchor treats USDC as 1 USD and prices USDC/TRY off the USD/TRY mid rate read from Reflector's foreign-exchange oracle on Stellar mainnet (contract CBKGPWGK…MCJZC, via simulateTransaction, cached 60 s). If the oracle is unreachable it falls back to a static rate and reports rate_source: "static_fallback".
A flat spread (default 50 bps) is applied on each side: buy_rate = mid × (1 + spread) when the user buys USDC, sell_rate = mid × (1 − spread) when they sell. There are no fixed fees. The three numbers are in GET /health.
A quote locks a rate and is single-use. Through SEP-38 a wallet can lock a firm rate before depositing or withdrawing (POST /sep38/quote), then pass its quote_id to the SEP-6 deposit / withdraw. A quote is optional: without one the deposit or withdraw prices at the live rate. Full detail and the SEP-38 price formulas are in SEP-38 quotes.
Flows
On-ramp (TRY → USDC)
sequenceDiagram
autonumber
participant U as User
participant W as Wallet / app
participant A as TR Mock Anchor
participant B as Bank (simulated)
participant S as Stellar testnet
W->>A: GET /auth → sign challenge → JWT (SEP-10)
W->>A: GET /sep6/deposit?asset_code=USDC&account=G…
A-->>W: instructions: IBAN + reference · transaction pending
U->>B: TRY transfer, açıklama = reference
B->>A: POST /sep6/tx/{id}/simulate-bank-transfer (you, in the sandbox)
A-->>W: TRY credited · rate locked (SEP-38 quote or live)
A->>S: payment or createClaimableBalance (USDC)
A-->>W: transaction completed + stellar_transaction_id
A SEP-6 deposit (with the SEP-10 JWT) takes asset_code=USDC and the destination account (a G… or muxed M…), and returns a transaction in pending_user_transfer_start with SEP-9 bank instructions. When the TRY arrives (you simulate it here), the anchor locks the rate (a SEP-38 quote_id if attached and valid, else live), debits TRY and pays real testnet USDC — a payment, or a claimable balance when there is no trustline (claimable_balance_id in the transaction). The status walks to completed with stellar_transaction_id. If the send fails permanently the transaction is error and the TRY is refunded.
Limits: No limit (testnet) per order (configurable; live values from /health).
Off-ramp (USDC → TRY)
sequenceDiagram
autonumber
participant U as User wallet
participant W as Wallet / app
participant A as TR Mock Anchor
participant S as Stellar testnet
W->>A: GET /auth → JWT (SEP-10)
W->>A: GET /sep6/withdraw?asset_code=USDC&type=bank_account
A-->>W: account_id (treasury) + memo (id), rate locked
U->>S: payment USDC → treasury, memo id
S-->>A: watcher sees the payment
A-->>W: USDC received, sold for TRY
A-->>W: TRY paid to IBAN (simulated FAST) · completed
A SEP-6 withdraw returns account_id (the treasury), memo_type: "id" and a 12-digit memo. The wallet must send USDC to that address with that memo (or to a muxed address whose id equals the memo). The anchor converts whatever amount actually arrives. The rate is locked for 30 minutes; later deposits are repriced at the current sell rate.
The TRY is paid to the user's IBAN (the one they sent via SEP-12, or a deterministic sandbox IBAN) as soon as the deposit lands — a simulated FAST transfer whose bank reference is recorded as external_transaction_id.
Settlement details
- Payment vs claimable balance. If the destination account exists and trusts the sandbox's USDC, the anchor sends a regular
payment. Otherwise (account unfunded, or no trustline) it creates a claimable balance with the destination as the sole, unconditional claimant. The wallet claims it after adding the trustline — see wallet-side notes. This is the same approach production ramps such as Etherfuse use for first-time wallets. - Muxed destinations. An
M…address is paid as such when the base account has a trustline; claimable balances always go to the baseG…account. - Memo on on-ramps. Optional text memo, truncated to 28 bytes.
- Treasury. One account pays every on-ramp and receives every off-ramp: see
GET /health. If it runs low, on-ramps wait withpending_reason: "treasury_low"and settle once it is refilled — they do not fail. - Idempotency. Quotes are single-use; each off-ramp has a unique memo; a deposit transaction is processed at most once.
SEP-6 door (wallets)
This section is the endpoint-level reference for the SEP door — the same flow the Concepts and Flows sections describe. A wallet, a wallet SDK or the Stellar demo wallet can use this anchor with zero custom code, discovering everything from stellar.toml.
Discovery & SEP-10
/.well-known/stellar.toml publishes TRANSFER_SERVER (/sep6), WEB_AUTH_ENDPOINT (/auth), KYC_SERVER (/sep12), ANCHOR_QUOTE_SERVER (/sep38), SIGNING_KEY and the USDC currency (anchor_asset="TRY").
SEP-10: GET /auth?account=G…[&memo=…][&client_domain=…] returns a challenge transaction (source = SIGNING_KEY, sequence 0, 15-minute window). Sign it with the account and POST /auth {"transaction": "<xdr>"}. The server verifies signatures against the account's signers and medium threshold (unfunded accounts: the master key exactly once), the optional client_domain signer, and answers {"token": "<JWT>"}. The JWT sub is G…, G…:memo or M…; send it as Authorization: Bearer to every SEP-6/12 endpoint and to SEP-38 quotes. Failures are HTTP 400 with {"error"}; missing tokens on protected endpoints are HTTP 403 {"type":"authentication_required"}.
SEP-12 (simulated KYC)
The sandbox never asks a real person for identity data. GET /sep12/customer for a wallet user that has not sent anything returns status: "NEEDS_INFO" with a fields map where every field is optional (first_name, last_name, email_address, bank_account_number, bank_name). Any PUT /sep12/customer — even an empty JSON object — flips the user to ACCEPTED and returns {"id"}. If a wallet sends a Turkish IBAN as bank_account_number it is validated and used for payouts; otherwise a deterministic sandbox IBAN stands in. tax_id, id_number, birth dates, photos and files are discarded on receipt. Memos separate users sharing an account (G…:memo); DELETE /sep12/customer/{account} forgets the optional data.
Deposit & withdraw
GET /sep6/info lists USDC for deposit, withdraw and the -exchange variants with funding_methods: ["bank_account"], fee_percent (the spread) and features.claimable_balances: true.
Deposit — GET /sep6/deposit?asset_code=USDC&account=G…&funding_method=bank_account[&amount=150.00] creates a transaction in pending_user_transfer_start and returns SEP-9 instructions:
{
"id": "sep_…",
"instructions": {
"bank_name": { "value": "TR Mock Bank A.Ş.", "description": "…" },
"bank_account_number": { "value": "TR05…", "description": "IBAN to send TRY to" },
"external_transfer_memo": { "value": "TRMA-7K2M-Q9XZ", "description": "Write this reference in the transfer description" }
},
"how": "…", "eta": 5, "fee_percent": 0.5,
"extra_info": { "message": "This is a sandbox… simulate the transfer at https://…/sep6/tx/sep_…" }
}
Because there is no bank, the TRY arrives when you say so: open the transaction's more_info_url (/sep6/tx/{id}) and press Simulate incoming TRY transfer, or POST /sep6/tx/{id}/simulate-bank-transfer {"amount":"150.00"}. The anchor credits the user, locks the rate (the SEP-38 quote_id if one was attached and still valid, else live), debits TRY and pays real testnet USDC to account — a payment when the trustline exists, otherwise a claimable balance (claimable_balance_id in the transaction).
Withdraw — GET /sep6/withdraw?asset_code=USDC&funding_method=bank_account[&amount=5] (the deprecated type=bank_account is accepted too) opens an off-ramp and returns account_id (the treasury), memo_type: "id" and memo. The wallet pays USDC with that memo; the watcher matches it on Horizon, sells at the locked rate and pays TRY (simulated FAST) to the user's IBAN. refund_memo/refund_memo_type are recorded.
Exchange variants — /sep6/deposit-exchange (source_asset=iso4217:TRY, destination_asset=stellar:USDC:<issuer>, amount, optional quote_id) and /sep6/withdraw-exchange (the reverse) make the pricing explicit and let the wallet lock a SEP-38 quote first.
History — GET /sep6/transactions?asset_code=USDC[&kind=deposit|withdrawal&limit=&no_older_than=&paging_id=] (newest first) and GET /sep6/transaction?id=|stellar_transaction_id=|external_transaction_id=. For withdrawals external_transaction_id is the payout's bank reference; for deposits it is the transfer reference.
SEP-38 quotes
Assets are iso4217:TRY (delivery method bank_account, country TUR) and stellar:USDC:<issuer>. GET /sep38/prices?sell_asset=…&sell_amount=… and GET /sep38/price?sell_asset=…&buy_asset=…&sell_amount=|buy_amount=…&context=sep6 are public; POST /sep38/quote (JWT) returns a firm quote valid 15 minutes by default (expire_after up to 1 hour). Prices obey the SEP-38 formulas: sell_amount = buy_amount × total_price and sell_amount − fee.total = price × buy_amount, with the fee (our spread) expressed in the sell asset. Quotes are single-use and bound to the authenticated user.
Statuses & callbacks
| SEP-6 status | Deposit | Withdrawal |
|---|---|---|
pending_user_transfer_start | Waiting for the TRY transfer (simulate it) | Waiting for the USDC payment with the memo |
pending_anchor | TRY received, paying USDC (also while the treasury is low) | — |
pending_stellar | Submission being retried | — |
completed | USDC paid; stellar_transaction_id, maybe claimable_balance_id | USDC received, TRY paid out; external_transaction_id = bank reference |
error | Send failed permanently; refunds shows the TRY returned to the balance | Cancelled |
Amounts: amount_in/amount_out with amount_in_asset/amount_out_asset, amount_fee (the spread, in TRY) and fee_details. Timestamps are ISO 8601. more_info_url is the human page for the transaction.
Pass on_change_callback=https://… on deposit/withdraw and the anchor POSTs {"transaction": …} on every status change with Signature: t=<unix>, s=<base64> — an Ed25519 signature by SIGNING_KEY over "<t>.<your host>.<body>":
import { Keypair } from '@stellar/stellar-sdk';
const [, t, s] = /t=(\d+), s=(.+)/.exec(req.headers['signature']);
const ok = Keypair.fromPublicKey(SIGNING_KEY).verify(Buffer.from(`${t}.${req.headers.host}.${rawBody}`), Buffer.from(s, 'base64'));
Testing with wallets & anchor-tests
- Demo wallet: at demo-wallet.stellar.org create and fund a testnet account, then Add asset with home domain
tr-mock-anchor.fly.devand codeUSDC. Use SEP-6 deposit: the wallet authenticates, shows the bank instructions and the more-info link — press the simulate button there. Use SEP-6 withdraw: the wallet pays the treasury with the memo and polls tocompleted. - anchor-tests:
npx stellar-anchor-tests --home-domain https://tr-mock-anchor.fly.dev --seps 1 10 12 6 38 --asset-code USDC --sep-config anchor-tests.config.json(the config file is in the repo). The four skipped tests only apply to anchors that run SEP-6 without authentication. - From code:
npm run e2e:sep6runs the full SEP flow from Node against any deployment and asserts the on-chain balances.
Wallet-side notes
- Trustline first. To receive USDC as a plain payment the wallet needs a trustline to
USDC:<issuer from /health>. Freighter (testnet) and Stellar Lab can add one. - Claiming. If a deposit settled as a
claimable_balance, the wallet adds the trustline and submitsclaimClaimableBalance({ balanceId })(Freighter shows claimable balances in its UI). Both can go in one transaction — the SEP demo does exactly this. - Memos on off-ramps. Always
memo_type: id. Wallets that cannot set a memo can pay a muxedM…address built from the treasury and the memo id (new MuxedAccount(treasury, memo).accountId()). - Exact amounts. The off-ramp converts what arrives, so partial or oversized payments still complete; only the memo must be right.
Reference
Stellar details
| Network | Testnet · passphrase Test SDF Network ; September 2015 |
|---|---|
| Horizon | https://horizon-testnet.stellar.org |
| Endpoints | Submission + treasury sequence via Stellar RPC (soroban-testnet.stellar.org, async send + poll); incoming-payment watcher via Horizon (RPC has no per-account payment history). See Mainnet for the production reasoning. |
| Asset | USDC issued by Circle's testnet issuer GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 (authoritative value: GET /health) |
| Treasury | See GET /health. Off-ramp deposits go here; on-ramps are paid from here. |
| stellar.toml | /.well-known/stellar.toml — SEP-1 with TRANSFER_SERVER, WEB_AUTH_ENDPOINT, KYC_SERVER, ANCHOR_QUOTE_SERVER, SIGNING_KEY |
| Testnet USDC for your wallets | faucet.circle.com → Stellar Testnet (20 USDC / address / 2 h). The SEP demo does not need it: it on-ramps first. |
Limits & configuration
| Setting | Default |
|---|---|
| Spread | 50 bps each side |
| SEP-38 quote validity | 15 min default (up to 1 h) |
| Off-ramp rate lock | 30 min, then repriced |
| On-ramp per order | No limit (testnet) |
| Off-ramp minimum | None (testnet) |
| Settlement cadence | on-ramps every 3 s, off-ramp detection every 5 s |
From sandbox to a real anchor: what will change
You built on the SEP path, so going live changes only the network passphrase (public) and the home domain. The SEP-1/10/6/12/38 endpoint shapes are the standard and do not change, so your integration code stays the same. The one dependency: the mainnet anchor must implement SEP-6. Behind that same surface, the anchor makes real what this sandbox simulates:
- Real KYC and compliance (SEP-12). Real identity verification, KYC tiers that set limits, travel-rule originator info on deposits, purpose / source of funds on withdrawals, and possibly pre-registered withdrawal addresses and bank accounts.
- Real bank rails. The
simulate-bank-transferhelper disappears; a real TRY transfer to the anchor's IBAN with the reference credits the deposit. You observe it, you do not trigger it. - Live pricing (SEP-38). Quotes come from a real order book, expire faster, and move with liquidity; fees may be line items rather than one blended spread.
- Limits and operations. Per-tier limits on every leg, rate limits (HTTP 429), maintenance windows and delayed settlement.
- Mainnet details. USDC's mainnet issuer is
GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN; trustlines, treasury addresses and explorers change. All of it is discovered from the mainnet anchor'sstellar.toml.
What carries over unchanged: SEP-10 login, the deposit-reference model, balances and ledgers, quote-then-execute thinking, address+memo routing for incoming crypto, IBAN payouts, status polling, and the discipline of treating every state in the status tables above.
FAQ
- Is this SEP-6 or SEP-24?
- SEP-6 (programmatic) — plus SEP-1, SEP-10, SEP-12 and SEP-38. SEP-24 (the anchor hosts the UI in a popup) is not implemented: with SEP-6 the integrator owns the deposit / withdraw UI, which is the shape this anchor targets.
- Can I use this from a browser app?
- Yes. CORS is open, and the SEP path is non-custodial: the user's wallet signs the SEP-10 challenge and the off-ramp payment in the browser. There is no API key to protect.
- My on-ramp is stuck in
pending. - Check
pending_reason.treasury_lowmeans the shared treasury needs USDC;retrying: …means Horizon hiccups. Both resolve on their own. - My off-ramp never completes.
- The payment probably lacked the memo, used
memo_type: text, or went to the wrong address. The anchor cannot attribute it — make sure the wallet sends the exactmemo_type: idmemo. - Why did I get 99.00 TRY back from 100 TRY?
- Two crossings of a 50 bps spread ≈ 1%. That is the anchor's revenue model.
- Is the data persistent?
- Yes, but this is a shared sandbox: assume it can be reset before events.
Glossary (TR / EN)
- Açıklama
- Transfer description field — where the customer writes the deposit reference.
- Havale / EFT / FAST
- Same-bank transfer / interbank transfer (banking hours) / instant 24-7 interbank transfer.
- IBAN
- 26-character Turkish bank account number:
TR+ 24 digits, mod-97 checksum. - T.C. Kimlik No (TCKN)
- 11-digit national identity number with a checksum; required for KYC in Türkiye.
- Bakiye
- Balance.
- Kur / Alış / Satış
- Exchange rate / buy rate / sell rate (from the customer's perspective).
- Yatırma / Çekme
- Deposit / withdrawal.
- Referans kodu
- Deposit reference code.
- Anchor
- Stellar term for a regulated entity bridging fiat and on-chain assets.
- Trustline
- A Stellar account's explicit opt-in to hold a given asset.
- Claimable balance
- Funds parked on-chain for a specific account to claim later.
- Memo (id)
- A 64-bit number attached to a Stellar payment, used here to route off-ramp deposits.