Anchors: the fiat spokes
Anchors are the fiat spokes. They hold the licences, the local rails and the float in each market; Corridor is the ledger and router in front of them.
Because every anchor speaks the same SEPs, adding a corridor is configuration — a home domain and an asset — rather than an integration. That is what makes the spokes additive, and what turns N×N corridors into N integrations (ADR 0006).
Configuring one
Section titled “Configuring one”const anchor = new SepAnchor({ domain: "anchor.example.com", // Optional: where to fetch stellar.toml, when that is not https at the // domain — a local Anchor Platform, or a tunnel. `domain` stays the // home_domain SEP-10 signs for. // baseUrl: "http://localhost:8080", asset: { code: "USDC", issuer: "GA5ZSEJY…KZVN" }, network: "mainnet", payoutCurrencies: ["KES"], signer: vaultSigner, // answers the SEP-10 challenge // Optional: SEP-12 ids, when you onboarded the parties out of band. // Leave them out and Corridor registers each payout's parties itself. // senderId, receiverId,});
registry.register(anchor);That is the whole integration. SepAnchor implements RampPartner, so the legs, the webhooks and reconciliation never learn it is an anchor rather than any other provider.
The SEPs Corridor speaks
Section titled “The SEPs Corridor speaks”| SEP | What Corridor does with it | What it needs from your stellar.toml |
|---|---|---|
| SEP-1 | Discovery: which endpoints and currencies you serve | The file itself |
| SEP-10 | Authentication: your challenge, signed by the vault key, exchanged for a JWT | WEB_AUTH_ENDPOINT, SIGNING_KEY |
| SEP-12 | Customer records: each payout’s parties are registered under the customer types your GET /info names, and whatever you still want is reported back by name |
KYC_SERVER |
| SEP-38 | Indicative prices and a firm quote before anything moves | ANCHOR_QUOTE_SERVER |
| SEP-31 | The cross-border payment itself, and GET /info for the funding methods you accept |
DIRECT_PAYMENT_SERVER |
| SEP-6 | Collections. Not implemented | TRANSFER_SERVER |
If an endpoint is missing, Corridor says exactly that — “anchor.example.com publishes no ANCHOR_QUOTE_SERVER, so it does not serve SEP-38” — rather than failing somewhere later.
SEP-12: KYC is passed through, not modelled
Section titled “SEP-12: KYC is passed through, not modelled”An anchor will not take a payment until it knows the parties, and it decides what knowing them means: it publishes a customer type per direction in its SEP-31 GET /info — sender_sep12_type and receiver_sep12_type — and a field list per type.
Corridor does not model anyone’s form. A payout carries two optional field bags, passed through untouched:
await anchor.openPayout({ currency: "KES", chain: "STELLAR", usdcAmount: 70_000_000n, beneficiary: { bankCode: "01", accountNumber: "0123456789" }, reference, sender: { first_name: "Uddy", email_address: "u@example.com" }, receiver: { first_name: "Amara", bank_account_number: "0123456789" },});Each side is registered under the type the anchor named, and the anchor’s verdict decides what happens next:
ACCEPTEDorPROCESSING— the customer id goes on the transaction.NEEDS_INFO— the payout is refused, and the refusal names the outstanding fields:minisend needs more about the receiver: bank_account_number, id_type, with the anchor’s own field descriptions indetails. A caller can collect exactly those and retry. Optional fields are left out of that list.REJECTED— final.
Nothing is sent to the anchor’s SEP-31 endpoint until both parties are settled, so a payout never half-opens waiting on paperwork.
Supply senderId and receiverId instead if a deployment onboarded its customers out of band; Corridor then skips SEP-12 entirely. And an anchor that names no customer type for a side gets no id for it — Corridor does not invent one.
SEP-10: the challenge is checked before the vault signs it
Section titled “SEP-10: the challenge is checked before the vault signs it”The vault key is the most valuable thing in a deployment, and SEP-10 asks it to sign a transaction the anchor supplies. SepAnchor verifies the challenge first:
- it must be built by the anchor’s own
SIGNING_KEY, as published in itsstellar.toml; - it must name this vault as the client account;
- it must be on the expected network passphrase.
Only then is it signed. The JWT is cached for its lifetime, with a 30-second margin.
SEP-38 then SEP-31: quote, then pay
Section titled “SEP-38 then SEP-31: quote, then pay”const order = await anchor.openPayout({ currency: "KES", chain: "STELLAR", usdcAmount: 70_000_000n, // 7 USDC at the hub's 7 dp beneficiary: { bankCode, accountNumber }, reference, // the 32-byte Corridor Reference});- SEP-38
POST /quotewithcontext: "sep31", sellingstellar:USDC:GA5Z…and buyingiso4217:KES. The rate is held for the life of the quote, so the beneficiary amount is known before anything moves. - SEP-31
POST /transactionswith the quote, the beneficiary fields and the Corridor Reference infields.transaction.reference. - The anchor returns
stellar_account_idand its ownstellar_memo— on the POST reply, or onGET /transactions/{id}a moment later, which Corridor waits for. The funding payment must carry that memo, so Corridor’s reference cannot ride inMEMO_HASHon this leg — it travels in the SEP-31 fields instead, and the join key becomes(anchor, transaction id)(ADR 0003). - The payout leg funds the order exactly once.
stellarAnchorFunderpays the receiving account out of the treasury carrying the anchor’s memo, and before sending it asks Horizon whether a payment with that memo, to that account, for that amount already landed — so a crash between sending and recording resumes instead of paying twice.
Status, and why there is no webhook
Section titled “Status, and why there is no webhook”SEP-31 has no webhook. The sending anchor polls GET /transactions/{id}, which is exactly what the saga’s wait loop does, so:
verifyWebhook(): boolean { return false; }parseWebhook(): never { throw new PartnerError("SEP-31 anchors are polled, not pushed"); }Corridor says so rather than pretending to support a delivery an anchor will never make. Statuses map onto Corridor’s four:
| SEP-31 status | Corridor |
|---|---|
pending_sender |
open |
pending_stellar, pending_receiver, pending_external, pending_customer_info_update, pending_transaction_info_update |
processing |
completed |
completed |
refunded, expired, error |
failed |
What is not built
Section titled “What is not built”What has run is the whole SEP path against a server Corridor does not control: examples/anchor brings up Stellar’s own Anchor Platform as a receiving SEP-31 anchor, and pnpm stellar:payout runs against it over the network — SEP-1, a SEP-10 challenge verified before the vault signs it, a firm SEP-38 quote, a SEP-31 order, a real Stellar payment carrying the anchor’s own id memo, the anchor’s observer seeing that payment by itself, and polling to completed with the books closing clean. The evidence page has the transaction.
Sixteen tests cover the adapter against a stubbed anchor on top of that.
Becoming a spoke
Section titled “Becoming a spoke”If you run a licensed anchor, what Corridor needs is:
A published stellar.toml
With WEB_AUTH_ENDPOINT, SIGNING_KEY, ANCHOR_QUOTE_SERVER and DIRECT_PAYMENT_SERVER, and the currencies you serve.
SEP-38 quotes against the settlement asset
stellar:USDC:<issuer> sold for iso4217:<your currency>, with context: "sep31".
A SEP-31 receiving account and memo per transaction
So every funding payment maps to exactly one order.
The reference echoed
Carry the 32-byte Corridor Reference from fields.transaction.reference into the bank narration, so a statement line traces back to the payout that caused it.
See the partner interface for the shape every provider is normalised to, and Payout EUR → KES for a full walkthrough.