# TORQ 연동 개선 요청 — Cryptoments

> 2026-08-14 · Cryptoments 백엔드 → TORQ(LaaS) 팀
>
> 요청 3건 + 운영 선행 2건. **1번이 본건**이고 나머지는 같은 뿌리에서 나온 것들이다.
> 모든 수치는 2026-08-14 Cryptoments 운영 DB 실측이다.

---

## 0. 요청 요약

| # | 요청 | 성격 |
|---|---|---|
| **1** | **거래 금액 정정 — 제자리(in-place) 방식으로 신설** | 신규 개발 |
| 2 | 분쟁 제기 웹훅에 사유 싣기 | 페이로드 보강 |
| 3 | 분쟁 해소 사유 싣기 (결과값 `resolution` 은 이미 잘 오고 있음) | 페이로드 보강 |
| A | `X-Internal-Token` 발급 | 운영 |
| B | 웹훅 서명 시크릿 합의 | 운영 · **보안** |

---

## 1. 거래 금액 정정 — 제자리 방식 신설

### 1.1 왜

구매자가 약정과 다른 금액을 입금했을 때 거래를 실입금액에 맞춰야 한다.

현행 `POST /internal/trades/correct-amount` 는 **취소 + 신규 escrow 대체** 방식이다. 2026-07-22 에 이 방식으로 한 건 처리했고(escrow 810 → 812, 70,000원 / 46.954140 USDT) 결과는 정상이었지만, **Cryptoments 쪽에 부작용 2건**이 남았다.

```
① 우리 매칭이 취소된 escrow 810 을 계속 가리켰다
   → 신규 escrow 는 우리 DB 에 없는 식별자라 완료 웹훅이 drop 된다(정상 동작)
   → 신규 거래 행을 수동 선삽입 + 합성 웹훅으로 이행해야 했다 (5단계 수동 절차)

② 릴리스 온체인 tx 를 우리 모니터가 '단독 입금'으로 오식별
   → 중복 입금 레코드 + 이중 크레딧 46.954140 발생 → 상쇄 분개로 사후 회수
   → 신규 escrow 는 매칭과 연결고리가 없어 분류기가 연결하지 못한다
```

**둘 다 식별자가 바뀌기 때문에 생긴다.**

### 1.2 요청 — `escrow_id` 를 유지하고 금액만 바꾼다

```
현행 (대체)                      요청 (제자리)
escrow 810  CANCELLED            escrow 810  IN_DISPUTE
escrow 812  신규 생성      →                  금액 정정 (810 유지)
                                             COMPLETED
```

식별자가 유지되면 우리 쪽 거래 행·매칭 연결·온체인 분류가 전부 그대로 살아 있어, 위 두 부작용이 원인 단계에서 사라진다.

**증액도 제자리로 요청한다.** LP 추가 공급 가부는 §1.5 의 에러코드로 응답해 주시면 된다. 공급이 안 되면 거부하시면 되고, 그 때문에 식별자를 바꿀 필요는 없다고 본다.

### 1.3 요청 스펙

```
POST /internal/trades/{escrowId}/correct-amount
X-Internal-Token: <발급 요청 — §A>
```

```json
{
  "correctionId": "corr_9f2a1c8b4d0e",
  "correctedKrwAmount": 70000,
  "reason": "AMOUNT_SHORT",
  "evidence": {
    "depositorName": "홍길동",
    "observedAt": "2026-07-22T14:02:11+09:00",
    "note": "판매자 계좌 실입금 70,000 확인"
  },
  "requestedBy": "admin:17"
}
```

| 필드 | 필수 | 설명 |
|---|---|---|
| `correctionId` | ✔ | **멱등키.** Cryptoments 발급. 동일 값 재요청은 동일 결과 |
| `correctedKrwAmount` | ✔ | 정정 후 KRW = 실입금액 |
| `reason` | ✔ | `AMOUNT_SHORT` / `AMOUNT_OVER` / `OTHER` |
| `evidence` | | 판정 근거 — LP 실입금 대조 시 참고 |
| `requestedBy` | ✔ | 요청 관리자 — 감사 추적 |

**USDT 금액은 보내지 않는다.** 확정 릴리스 금액의 권위는 TORQ 에 있다(2026-08-07 합의). 우리가 계산해 보내면 값이 두 곳에서 나오게 된다.

**환율은 거래 성립 시점의 `effectiveFxRate` 를 유지**해 주시기 바란다. 거래는 그때 성립했고 금액만 다르게 들어온 것이라, 재조회하면 시점 차이로 한쪽이 유불리를 본다.

```
correctedUsdtAmount = correctedKrwAmount ÷ (그 거래에 고정된 effectiveFxRate)
```

> 2026-08-07 에 확인된 건이라 함께 적어둔다 — `effectiveFxRate` 는 **exclusive**(`market ÷ 0.98`)다. inclusive(`market × 1.02`)로 계산하면 +0.0408% 과대가 된다. 정정 산식도 exclusive 인지 확인 부탁드린다.

### 1.4 응답

```json
{
  "escrowId": 810,
  "correctionId": "corr_9f2a1c8b4d0e",
  "status": "CORRECTED",
  "before": { "krwAmount": 700000, "usdtAmount": "469.541400" },
  "after":  { "krwAmount":  70000, "usdtAmount":  "46.954140" },
  "effectiveFxRate": "1490.8163",
  "tradeStatus": "IN_DISPUTE",
  "correctedAt": "2026-07-22T14:05:33+09:00"
}
```

`after.usdtAmount` 가 확정 릴리스 금액이며, 이후 완료 웹훅의 `data.usdtAmount` 와 일치해야 한다.

### 1.5 에러 — 거부는 정상 응답이다

| code | HTTP | 의미 |
|---|---|---|
| `321` | 409 | 요청 금액이 LP 실입금과 불일치 |
| `322` | **200** | 멱등 — 이미 같은 `correctionId` 로 정정됨 |
| `323` | 409 | 자동 판단 불가 — TORQ 수동 확인 필요 |
| `324` | 409 | 증액 불가 — LP 공급 부족 |
| `325` | 409 | 현재 상태에서 정정 불가 (§1.6) |
| `326` | 409 | 이미 다른 `correctionId` 로 정정됨 |

`321`~`323` 은 기존 코드이며 의미가 위와 같은지 확인 부탁드린다. `324`~`326` 은 신설 요청이다.

> **`322` 를 200 으로 주시기를 요청한다.** 재시도가 실패로 보이면 관리자가 같은 요청을 반복하게 되고, 그때마다 판단이 갈린다.

에러 바디는 기존 형식(`{"code","message"}`)을 따르면 된다.

### 1.6 정정 가능 상태

```
ACCEPTED      ✔  구매자 이체 전 — 금액 합의 변경
TRANSFERRED   ✔  이체 신고됨, LP 확인 전 — 가장 흔한 경우
IN_DISPUTE    ✔  분쟁 중 — 2026-07-22 사례가 여기
CREATED       ✘  LP 배정 전 — 취소 후 재생성이 맞다
COMPLETED     ✘  릴리스 완료 — 회수는 정정이 아니라 반환 절차라 별건
CANCELLED     ✘  종결
```

TORQ 상태 머신과 맞는지 확인 부탁드린다.

### 1.7 정정이 곧 분쟁 해소다

`IN_DISPUTE` 에서 정정이 수락되면 **별도 판정 단계 없이 그대로 릴리스**해 주시기를 요청한다.

```
IN_DISPUTE ──정정 수락──▶ AMOUNT_CORRECTED ──▶ 릴리스 ──▶ COMPLETED
```

분쟁의 내용이 "얼마가 들어왔나"이고 정정이 그 답이므로, 수락해 놓고 다시 판정을 묻는 것은 같은 질문을 두 번 하는 것이 된다. 단계를 더 두면 그 사이에 멈춘 거래가 생긴다.

```
정정 수락 = 분쟁 RESOLVED + resolution = RELEASE_TO_BUYER
정정 거부 = 분쟁 유지 (기존 판정 절차로 복귀)
```

### 1.8 정정 통지 웹훅 — 릴리스보다 먼저

관리자가 요청한 정정이라도 **웹훅으로 다시 받아야 한다.** 요청 응답만 신뢰하면 `323`(수동 확인) 후 결론이 뒤집힌 경우를 받을 수 없다.

```
POST /webhooks/torq/{provider}
```

```json
{
  "eventType": "AMOUNT_CORRECTED",
  "escrowId": 810,
  "data": {
    "correctionId": "corr_9f2a1c8b4d0e",
    "beforeKrwAmount": 700000,
    "afterKrwAmount": 70000,
    "beforeUsdtAmount": "469.541400",
    "afterUsdtAmount": "46.954140",
    "effectiveFxRate": "1490.8163",
    "reason": "AMOUNT_SHORT",
    "correctedBy": "TORQ_ADMIN",
    "correctedAt": "2026-07-22T14:05:33+09:00"
  }
}
```

기존 웹훅 규약 그대로다 — `X-Torq-Signature` · `X-Torq-Timestamp` · `X-Torq-Event-Id`.

> ⚠️ **`AMOUNT_CORRECTED` 가 `COMPLETED` 보다 먼저 도착해야 한다.**
>
> 우리 정산은 완료 웹훅을 받는 즉시 실행된다. 그 시점에 우리 쪽 금액이 아직 원래 값이면 **정정 전 금액으로 크레딧된다** — 2026-07-22 에 미보정 상태였다면 469.54 USDT 가 나갈 뻔했다.
>
> §1.7 로 정정과 릴리스가 한 덩어리가 되더라도, **통지는 두 번**이고 순서가 보장되어야 한다. 간격은 짧아도 무방하다.

### 1.9 완료 웹훅에 정정 표식

현재 완료 웹훅의 `usdtAmount` 가 **원값인지 정정값인지 구분할 필드가 없다.**

```json
{
  "eventType": "COMPLETED",
  "escrowId": 810,
  "data": {
    "txHash": "262f44bf...",
    "senderWallet": "T...",
    "usdtAmount": "46.954140",
    "resolution": "RELEASE_TO_BUYER",

    "amountCorrected": true,
    "correctionId": "corr_9f2a1c8b4d0e",
    "originalUsdtAmount": "469.541400"
  }
}
```

아래 세 필드를 **정정이 있었던 경우에만** 실어 주시기를 요청한다. 없으면 현재와 동일하게 동작하므로 하위호환이 유지된다.

```
amountCorrected · correctionId · originalUsdtAmount
```

이게 있으면 대사(對査) 시 "금액이 왜 다른가"를 조인 없이 판별할 수 있다.

---

## 2. 분쟁 제기 웹훅에 사유

### 실측

```
Cryptoments 가 기록한 TORQ 레그 분쟁      37건
  그중 사유가 남은 것                      3건
  사유 없음                               34건
```

`IN_DISPUTE` 웹훅의 `data.reason` 이 대부분 비어서 온다.

### 요청

```
data.reason 을 필수로 — 최소한
  NO_DEPOSIT / AMOUNT_SHORT / AMOUNT_OVER / OTHER
```

### 왜 필요한가

사유가 없으면 우리 관리자가 **분쟁을 보고도 무엇을 확인해야 할지 모른다.** 특히 "미입금"과 "금액 불일치"의 구분이 §1 정정 요청 여부를 가르는데, 그 구분이 전달되지 않는다.

---

## 3. 분쟁 해소 사유

### 먼저 — 결과값은 잘 오고 있다

내부 문서에 "완료 웹훅에 `resolution` 이 없다"고 적혀 있었으나, **실측 결과 사실이 아니었다.** 정정해서 전달한다.

```
분쟁을 겪은 TORQ 레그                     38건
  이후 COMPLETED                          22건 → resolution 있음 21 (RELEASE_TO_BUYER)
  이후 CANCELLED                          15건 → resolution 있음 15 (REFUND_TO_BUYER)
```

거의 전량 전달되고 있다. **이 항목은 요청에서 내린다.**

### 남는 요청 (우선순위 낮음)

`resolution` 은 **결과**(무엇으로 끝났나)이지 **사유**(왜 그렇게 판정했나)가 아니다. 관리자가 사후에 판정 근거를 확인할 수 있도록, 가능하다면 해소 사유 텍스트를 함께 실어 주시기를 요청한다.

```json
"resolution": "RELEASE_TO_BUYER",
"resolutionReason": "판매자 계좌 입금 확인 (2026-07-22 14:02)"
```

**1·2번보다 우선순위가 낮다.** 여력이 있을 때 검토해 주시면 된다.

---

## A. `X-Internal-Token` 발급

`/internal/*` 경로용 토큰을 Cryptoments 는 아직 보유하고 있지 않다. §1 의 선행 조건이다.

기존 위젯 API 의 `X-Api-Key` / `X-Signature` 와는 별개 경로로 두기를 제안한다 — 관리자 조작이라 권한 등급이 다르다.

---

## B. 웹훅 서명 시크릿 — 보안

**현재 Cryptoments 운영 환경에 TORQ 웹훅 시크릿이 설정돼 있지 않아 서명 검증이 스킵되고 있다.**

우리 쪽 문제이며 우리가 설정하면 되지만, **시크릿 값 합의가 필요**하다.

§1 의 금액 정정 인터페이스는 **자금 금액을 바꾸는 웹훅**이므로 검증 없이 수신해서는 안 된다. **§1 도입의 선행 조건으로 처리**하고자 한다.

검증 방식은 기존 규약 그대로다.

```
X-Torq-Signature   sha256=hex(HMAC-SHA256(secret, timestamp + "." + body))
X-Torq-Timestamp   unix epoch sec (±300초)
X-Torq-Event-Id
```

---

## C. TORQ2

Cryptoments 는 총판 그룹별로 LP 를 나누는 구조를 준비 중이며, TORQ2 를 별도 provider 로 둘 예정이다(같은 시스템·다른 체인으로 이해하고 있다).

확인 부탁드린다.

```
① TORQ2 도 위 §1~§3 규격을 동일하게 구현하는가 (같은 코드베이스면 자동일 것으로 예상)
② TORQ2 의 체인이 BSC 로 확정인가
③ TORQ2 는 리베이트 대상이 아닌 것으로 이해하고 있다 — 맞는가
④ TORQ2 의 base-url · API 자격증명 · 웹훅 시크릿
```

---

## D. 확인 요청 정리

| # | 항목 | 블로킹 |
|---|---|---|
| 1 | **제자리 정정이 에스크로 계약·정산 구조상 가능한가** (§1.2) | ✔ 본건 |
| 2 | `X-Internal-Token` 발급 (§A) | ✔ |
| 3 | 웹훅 시크릿 합의 (§B) | ✔ |
| 4 | 에러코드 `324`~`326` 신설 · `321`~`323` 의미 확인 (§1.5) | |
| 5 | 정정 가능 상태가 TORQ 상태 머신과 맞는지 (§1.6) | |
| 6 | 정정 수락이 분쟁을 해소하고 릴리스까지 가는 것 (§1.7) | |
| 7 | `AMOUNT_CORRECTED` 가 `COMPLETED` 보다 먼저 도착 보장 (§1.8) | ✔ 자금 |
| 8 | `effectiveFxRate` exclusive 적용 확인 (§1.3) | |
| 9 | 분쟁 제기 사유 필수화 일정 (§2) | |
| 10 | TORQ2 4개 항목 (§C) | |

> **1번이 부정이면 대체(신규 escrow) 방식으로 되돌아간다.** 그 경우 escrow 교체 이력을 우리 쪽에서 모델링해야 하므로, 회신을 받은 뒤 우리 구현 범위를 다시 잡겠다.

---

**문의**: Cryptoments 백엔드
**관련 사례**: 2026-07-22 `pdo_39109fcb9066` / escrow 810 → 812
