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:

  1. Your user logs in with their Stellar key (SEP-10). KYC is simulated (SEP-12) and needs no personal data.
  2. The user starts a deposit and gets bank details: the anchor's IBAN and a personal reference code to write in the description.
  3. The TRY transfer arrives with that reference → the user has a TRY balance.
  4. It converts at the USD/TRY rate and real testnet USDC is paid on Stellar to their wallet.
  5. Off-ramp is the reverse: send USDC with a memo → TRY → payout to the IBAN.
Try it live: the SEP demo runs the full round trip from your browser with real testnet transactions. The SEP path page documents the raw calls.

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

PieceReal?Notes
Stellar payments to wallets (on-ramp)RealTestnet USDC from the treasury; payment or claimable balance
Detecting USDC deposits (off-ramp)RealHorizon payments stream, matched by memo id / muxed id
RatesReal (indicative)Reflector USD/TRY oracle on Stellar mainnet, plus a flat spread
Balances, ledger, quotesReal logicSame behaviour a production system would have
Incoming TRY bank transfersSimulatedYou call the sandbox endpoint that a bank integration would call
TRY payouts to IBANsSimulatedInstant record with a FAST-style reference
KYC / complianceSimulatedSEP-12: NEEDS_INFO until any PUT, then ACCEPTED; no personal data required, identity numbers never stored
SEP-10 auth, SEP-6/38 protocol surfaceRealVerified 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.

Full SEP walkthrough: the SEP path. Run it live: the SEP demo, or demo-wallet.stellar.org with 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 deposit returns SEP-9 instructions: 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's more_info_url button). 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:

kindcurrencywhen
bank_transferTRY +a TRY transfer was matched to the customer
onrampTRY −on-ramp created (debited up front)
onramp_refundTRY +on-ramp failed permanently
offramp_depositUSDC +USDC arrived for an off-ramp
offramp_convertUSDC − / TRY +the deposit was sold for TRY
payoutTRY −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.

Payments without a usable memo, with an unknown memo, or to a cancelled/completed withdrawal cannot be attributed. In production a support process would return or assign them — make your UI show the memo prominently.

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 base G… 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 with pending_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.

SEP-6 deposits and withdrawals do not require the SEP-12 step: authenticating is enough. SEP-12 exists so wallets that always run KYC first keep working.

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 statusDepositWithdrawal
pending_user_transfer_startWaiting for the TRY transfer (simulate it)Waiting for the USDC payment with the memo
pending_anchorTRY received, paying USDC (also while the treasury is low)—
pending_stellarSubmission being retried—
completedUSDC paid; stellar_transaction_id, maybe claimable_balance_idUSDC received, TRY paid out; external_transaction_id = bank reference
errorSend failed permanently; refunds shows the TRY returned to the balanceCancelled

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

  1. Demo wallet: at demo-wallet.stellar.org create and fund a testnet account, then Add asset with home domain tr-mock-anchor.fly.dev and code USDC. 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 to completed.
  2. 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.
  3. From code: npm run e2e:sep6 runs 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 submits claimClaimableBalance({ 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 muxed M… 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

NetworkTestnet · passphrase Test SDF Network ; September 2015
Horizonhttps://horizon-testnet.stellar.org
EndpointsSubmission + 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.
AssetUSDC issued by Circle's testnet issuer GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 (authoritative value: GET /health)
TreasurySee 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 walletsfaucet.circle.com → Stellar Testnet (20 USDC / address / 2 h). The SEP demo does not need it: it on-ramps first.

Limits & configuration

SettingDefault
Spread50 bps each side
SEP-38 quote validity15 min default (up to 1 h)
Off-ramp rate lock30 min, then repriced
On-ramp per orderNo limit (testnet)
Off-ramp minimumNone (testnet)
Settlement cadenceon-ramps every 3 s, off-ramp detection every 5 s

From sandbox to a real anchor: what will change

This topic has a dedicated page with a readiness checklist: Mainnet: what to expect.

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-transfer helper 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's stellar.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_low means 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 exact memo_type: id memo.
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.