MoltCarlo 연동 가이드 (B2B)
귀사(operator) 사이트에 MoltCarlo를 iframe으로 넣고, 귀사 사용자의 잔고로 예측 베팅을 할 수 있게 연동하는 방법입니다. 핵심 모델은 Seamless Wallet + 지연 정산입니다. 본 문서는 개발자용 안내이며, 실제 연동 키·엔드포인트는 온보딩 시 발급됩니다.
용어 정의 — 먼저 읽어주세요
- operator (귀사)
- MoltCarlo를 자사 사이트에 iframe으로 임베드하는 파트너 사이트. API의 operator_id가 가리키는 주체입니다. 이 문서에서 ‘귀사’는 곧 operator를 뜻합니다.
- MoltCarlo
- iframe 안에서 예측 베팅을 제공하는 우리 서비스.
- 위젯 (widget)
- operator 페이지에 임베드되는 MoltCarlo iframe — 실제 거래 화면.
- 사용자 (end user)
- operator의 고객. operator가 인증하며, MoltCarlo는 별도 로그인을 받지 않습니다.
- operator 지갑 (wallet)
- 사용자 잔고가 보관되는 곳. 잔고는 항상 operator가 기준(source of truth)이고, MoltCarlo는 잔고를 보관하지 않습니다.
- 베팅 원장 (bet ledger)
- MoltCarlo가 보관하는 베팅·정산 기록입니다(잔고가 아님).
1. 모델 한눈에 — 왜 ‘지연 정산’인가
카지노 게임(Evolution, Pragmatic 등)은 보통 진입 시 operator 지갑을 잠금(lock) → 플레이 → 종료 시 정산 → 잠금 해제하는 방식입니다. 라운드가 사용자가 보는 앞에서 끝나기 때문입니다.
하지만 예측 베팅은 결과가 한참 뒤에 나옵니다. 사용자가 iframe을 닫고 며칠 뒤에 확정될 수 있습니다. 그래서 ‘종료 시 정산’ 모델은 맞지 않고, 스포츠북(고정배당)과 같은 지연 정산(Seamless Wallet)을 사용합니다:
- 잔고는 항상 귀사 데이터가 기준(source of truth)입니다. MoltCarlo는 사용자 잔고를 보관하지 않고 베팅 원장만 갖습니다.
- 베팅 시점에 귀사 지갑에서 즉시 차감(debit) — 홀드(lock)가 아니라 실제 차감입니다.
- 결과 확정 시점에 당첨금을 적립(credit) — 세션·iframe과 무관하게 서버 간 통신으로, 원래 베팅을
refTxUuid로 참조합니다.
2. 직접 체험 — 플로우 시뮬레이터
아래에서 operator(귀사) 입장의 돈 흐름을 단계별로 눌러보세요. 베팅 시 차감, iframe 닫힘, 며칠 뒤 결과 확정 시 적립까지 — 지갑 호출 로그로 확인할 수 있습니다.
실제 위젯도 바로 실행해 보세요:
실제 launch → 세션 → 베팅(데모 지갑 차감) → 결과 확정 시 정산이 일어나는 라이브 샌드박스입니다(데모 잔고 ₩1,000,000으로 시작).
3. 연동 4단계
- 온보딩 — MoltCarlo가 귀사에
operator_id,api_key, 서명 키(운영은 비대칭 RSA/Ed25519, 샌드박스는 HMAC), 그리고 귀사의walletUrl·허용 IP·임베드 출처(allowedOrigins)·베팅 한도(스테이크)를 등록합니다. - Launch — 사용자가 위젯을 열 때, 귀사 백엔드가 서명된 요청을 보내 1회용 위젯 URL을 받고 그 URL을 iframe
src로 사용합니다. (4·5절) - Wallet 콜백 구현 — 귀사가
balance·bet(debit)·win(credit)·rollback네 개의 콜백을 구현합니다. MoltCarlo가 베팅·정산 때 이걸 호출합니다. (6절) - 정산 — 결과가 확정되면 MoltCarlo가
win을 호출해 적립합니다(refTxUuid로 원래 베팅 참조). 사용자가 접속 중이 아니어도 됩니다.
4. Launch 핸드셰이크
귀사 백엔드 → MoltCarlo. 요청 본문을 귀사 서명 키로 서명하고 X-MC-Signature 헤더에 담습니다. 본문에는 재전송 공격(replay) 방지를 위해 timestamp와 nonce를 포함합니다.
요청 본문
| 필드 | 타입 | 설명 |
|---|---|---|
| operator_id | string | 발급받은 operator 식별자 |
| user | string | 귀사 사용자의 고유 ID (외부 ID로 매핑됨) |
| token | string | 귀사의 사용자 세션 토큰 — MoltCarlo가 지갑 호출 시 되돌려줌 |
| currency | string | 예: KRW (샌드박스 데모는 "XXX") |
| lang | string? | ko / en / ja (기본 ko) |
| timestamp | number | 유닉스 ms, 5분 skew 초과 시 거절 |
| nonce | string | 1회성 임의값 — 재전송 공격(replay) 방지 |
응답
{ "url": "https://moltcarlo.com/widget#lt=<1회용-토큰-120초>" }이 url을 iframe src로 넣으면, 위젯이 1회용 lt를 세션 토큰으로 교환하고(원자적으로 한 번만 사용) 거래 화면이 뜹니다. 세션 토큰은 쿠키가 아니라 헤더(X-MC-Session) 로 전달되어 3rd-party 쿠키 차단 환경에서도 동작합니다.
5. 코드 샘플 — Launch 요청 (귀사 백엔드)
import crypto from "crypto";
const OPERATOR_ID = "op_yoursite";
const SECRET = process.env.MC_SECRET; // 온보딩 시 발급 (샌드박스: HMAC)
const MC_BASE = "https://moltcarlo.com";
async function getWidgetUrl(user, userToken) {
const body = {
operator_id: OPERATOR_ID,
user, // 귀사 사용자 ID
token: userToken, // 귀사 사용자 세션 토큰
currency: "KRW",
lang: "ko",
timestamp: Date.now(),
nonce: crypto.randomUUID(),
};
const payload = JSON.stringify(body);
const sig = crypto.createHmac("sha256", SECRET).update(payload).digest("hex");
const res = await fetch(MC_BASE + "/api/op/v1/launch", {
method: "POST",
headers: { "content-type": "application/json", "X-MC-Signature": sig },
body: payload,
});
const { url } = await res.json();
return url; // <iframe src={url}> 로 사용
}⚠️ url(lt 포함)은 1회용·120초입니다. 토큰은 URL 프래그먼트(#)에 담겨 Referer 헤더·서버 접근 로그에 남지 않습니다.
6. Wallet 콜백 — 귀사가 구현 (잔고 기준 데이터)
MoltCarlo → 귀사 walletUrl. 네 가지 콜백을 구현합니다. 모든 호출은 서명되어 오고(검증 필수), transaction_uuid를 멱등키로 사용합니다. 금액은 정수 minor unit(1원 = 1, 소수점 금지)입니다.
| 필드 | 타입 | 설명 |
|---|---|---|
| POST /balance | 조회 | → { balance } (현재 잔고) |
| POST /bet | 차감(debit) | 베팅 스테이크 차감. 잔고 부족 시 RS_ERROR_NOT_ENOUGH_MONEY |
| POST /win | 적립(credit) | 정산/매도 당첨금 적립. reference_transaction_uuid = 원래 bet |
| POST /rollback | 되돌림 | 차감 실패/취소 시 원래 bet을 되돌림. reference_transaction_uuid 참조 |
공통 요청/응답
| 필드 | 타입 | 설명 |
|---|---|---|
| operator_id | string | 귀사 식별자 (서명·api_key와 일치해야 함) |
| user | string | 귀사 사용자 ID |
| token | string | Launch 때 넘긴 귀사 사용자 토큰 (bet/balance에서 사용) |
| transaction_uuid | string | 이 호출의 멱등키 (재시도 시 같은 결과 반환) |
| reference_transaction_uuid | string? | win/rollback이 참조하는 원래 bet의 uuid |
| round | string | 라운드 ID — 베팅이 속한 마켓 단위 식별자 |
| amount | integer | minor unit (1원=1) |
| currency | string | KRW |
응답은 { status: "RS_OK", balance, request_uuid } 형태로, 받은 request_uuid를 그대로 echo 합니다. 상태값:
RS_OK· 정상RS_ERROR_NOT_ENOUGH_MONEY· 잔고 부족RS_ERROR_DUPLICATE_TRANSACTION· 멱등 중복RS_ERROR_INVALID_SIGNATURE· 서명 오류
app.post("/wallet/:op", async (req, res) => {
// 1) 서명 검증 — 실패 시 RS_ERROR_INVALID_SIGNATURE
if (!verify(req.rawBody, req.header("X-MC-Signature"))) {
return res.json({ status: "RS_ERROR_INVALID_SIGNATURE" });
}
const b = req.body;
// 2) operator_id / api_key / 서명키가 모두 같은 operator인지 확인
// 3) 멱등: 같은 transaction_uuid를 이미 처리했으면 '그때 결과'를 반환
const prev = await findTxn(b.transaction_uuid);
if (prev) return res.json(prev.result); // 절대 중복 반영 금지
if (req.path.endsWith("/bet")) { // 차감
const bal = await getBalance(b.user);
if (bal < b.amount)
return res.json({ status: "RS_ERROR_NOT_ENOUGH_MONEY", balance: bal });
const after = await debit(b.user, b.amount, b.transaction_uuid, b.round);
return res.json(save(b, { status: "RS_OK", balance: after }));
}
if (req.path.endsWith("/win")) { // 적립 (정산)
// ✅ 반드시: reference_transaction_uuid에 해당하는 '직전 bet'이 존재하고
// 서명이 유효할 때만 적립. (user, refTxUuid)만으로 적립하면 위조 위험!
const after = await credit(b.user, b.amount, b.transaction_uuid,
b.reference_transaction_uuid);
return res.json(save(b, { status: "RS_OK", balance: after }));
}
// rollback 도 동일 패턴: refTxUuid의 bet을 되돌림, 멱등.
});7. 보안 체크리스트 (꼭 지키세요)
- 서명 검증 — 모든 인바운드 콜백의 서명을 검증. 운영은 비대칭 키(키가 유출돼도 상대 호출 위조 불가).
- 멱등(Idempotency) —
transaction_uuid로 중복 처리 방지. 재시도 시 그때의 결과를 반환(새로 반영 금지). 멱등 검증은(uuid, type, amount)까지 확인. - 적립은 직전 debit 매칭 후에만 —
win은reference_transaction_uuid의 bet이 실제로 존재하고 서명이 유효할 때만 적립.(user, refTxUuid)만으로 승인하면 정산 위조에 노출됩니다. - 금액은 정수 minor unit — float 금지, 음수·과대값 가드.
- 출처·IP 고정 — 임베드 출처(
allowedOrigins)· 허용 IP는 온보딩 시 등록된 값만.postMessage는 등록 출처 로만. - 잔고 변경은 클라이언트가 할 수 없음 — 모든 차감/적립은 서버 간(S2S) 통신입니다. 위젯(브라우저)이 보내는 값으로 잔고가 바뀌지 않습니다.
8. operator 관리 페이지 & 인증
네, operator 전용 관리 포털이 있습니다 → /op-portal. 샌드박스는 operator_id + api_key로 로그인합니다 (데모: op_demo / op_demo_key). 로그인하면 잔고 노출·미결 베팅·정산 상태·연동 키·한도를 확인할 수 있습니다.
- 지금 제공: 미결 노출·베팅액, 상태별 베팅, 최근 베팅 내역, 연동 키(마스킹)·walletUrl·한도 조회, 지급 실패(STRANDED) 경보
- 준비 중: 키 회전(rotate), 대사(정산 대조)용 CSV, 콘텐츠 opt-out, 지급 실패 건 재처리, 샌드박스/라이브 전환
인증은 3계층입니다
| 필드 | 타입 | 설명 |
|---|---|---|
| ① 머신 (귀사 백엔드 ↔ MoltCarlo) | api_key + 서명 + IP + nonce | launch·지갑 콜백의 서버 간(S2S) 인증 |
| ② 포털 (귀사 직원 ↔ /op-portal) | operator_id + api_key (샌드박스); 운영은 직원 로그인+2FA | operatorId로 스코프된 백오피스 |
| ③ 엔드 사용자 (귀사 사용자 ↔ iframe) | launch 토큰 | 별도 로그인 없음 — 귀사가 인증한 사용자의 신원을 서명 토큰으로 위임 |
즉, 귀사 사용자는 MoltCarlo에 가입·로그인하지 않습니다. 귀사가 자체 인증한 사용자를 launch 토큰으로 넘기면, MoltCarlo는 서명을 검증하고 (operatorId, 사용자ID)로 매핑합니다.
9. 샌드박스 → 라이브
MoltCarlo가 레퍼런스 지갑(샌드박스)을 호스팅하므로, 귀사 지갑 구현 없이도 전체 루프(launch → 베팅 차감 → 며칠 뒤 정산 적립 → operator 잠깐 다운 시 재시도)를 먼저 검증할 수 있습니다. 인증 체크리스트 (특히 ‘세션 없이 refTxUuid로 정산’, 멱등, 서명)를 통과하면 라이브로 전환합니다.
연동 문의: contact@moltcarlo.com
본 문서는 개발자 안내용이며 사전 고지 없이 갱신될 수 있습니다. (초안 2026-06-28)