# x402-bind

x402 로 **사고 판다**. 공식 SDK 가 이미 있는데 이걸 따로 만든 이유는 하나다 —
우리가 파는 건 SDK 가 아니라 그 위의 두 가지, **실물 이행**과 **판정**이다.
402 와 검증이 남의 라이브러리 뒤에 있으면 고쳐야 할 자리를 못 본다.

```bash
pip install x402-bind
```

라이브 상점: <https://x402.tcg-bind.com> — 같은 URL 이 사람에게는 페이지를, 기계에는 JSON 을 준다.

---

## 사는 쪽

```bash
x402-bind new                  # 지갑을 만든다 (600 권한, 키는 파일에만)
x402-bind address
x402-bind quote https://x402.tcg-bind.com/items/PKM-SV8PT5-BB/buy
x402-bind buy   https://x402.tcg-bind.com/items/PKM-SV8PT5-BB/buy --max 100.00
```

```python
from x402_bind import Buyer

buyer = Buyer(open("wallet.key").read(),
              max_amount="100.00",                 # 한도는 코드로 강제된다
              allow_networks=["eip155:4663"])      # 그 밖은 서명 자체를 안 만든다

r = buyer.get("https://x402.tcg-bind.com/items/PKM-SV8PT5-BB/buy")
if r:                       # Result 는 paid 로 불리언 평가된다
    print(r.spent)          # {'amount': '83.000000 USDG', 'network': 'eip155:4663', ...}
    print(r.transaction)    # 온체인 해시
    print(r.order)          # 주문번호 · 배송지 제출 링크
else:
    print(r.reason)         # 왜 안 됐는지. 삼키지 않는다.
```

**가스는 낼 필요가 없다.** EIP-3009 는 서명과 제출을 분리한다 — 서명만 하면 판매자 쪽
퍼실리테이터가 제출하고 가스를 낸다. 지갑에는 스테이블코인만 있으면 된다.

### 이 클라이언트가 지키는 세 가지

| | |
|---|---|
| **한도를 코드로 강제한다** | 에이전트는 재시도하고, 프롬프트 인젝션에 걸리고, 루프에 빠진다. `max_amount` 밖은 **서명 자체를 만들지 않는다.** "조심하라"고 문서에 쓰는 건 한도가 아니다. |
| **무엇을 냈는지 반드시 돌려준다** | 돈을 쓰는 라이브러리가 조용하면 안 된다. 성공하면 `spent`·`transaction`·`order` 가 온다. |
| **실패를 삼키지 않는다** | 결제가 안 됐으면 `paid=False` 와 이유를 그대로 준다. CLI 는 종료코드 `2` 를 낸다. 예외를 먹고 빈 결과를 주면 에이전트가 "샀다"고 보고하게 된다. |

```bash
$ x402-bind buy <url> --max 5.00
{ "paid": false,
  "reason": "낼 수 있는 레일이 없다: eip155:4663: 83.000000 USDG > 한도 5.00; ..." }
$ echo $?
2
```

---

## 파는 쪽

```python
from x402_bind import Store, Item, Rail

RH = Rail(caip2="eip155:4663", chain_id=4663,
          asset="0x5fc5360d0400a0fd4f2af552add042d716f1d168",
          symbol="USDG", decimals=6,
          eip712_name="Global Dollar", eip712_version="1",   # 온체인 대조로 확정한 값
          pay_to="0x...")                                     # 콜드 수령 주소

store = Store([RH], verify=my_verify, settle=my_settle)
store.add(Item(sku="CARD-1", title="…", price="83.00", shipping="0.00", stock=10,
               market_price={"value": "83.60", "source": "tcgplayer",
                             "as_of": "2026-09-26", "method": "third_party_feed"}))

# 프레임워크에 묶이지 않는다. (status, body, headers) 를 돌려줄 뿐이다.
status, body, headers = store.buy(sku, request_url, request.headers.get("X-PAYMENT"))
```

### 이 SDK 가 강제하는 다섯 가지

전부 실제로 데인 자리다.

**1. 값을 모르는 걸 팔지 않는다.**
가격이 없으면 402 로 아무 금액이나 요구하지 않고 `409 not for sale` 을 낸다.
*(2026-09-25: 가격 미설정 상품에서 서버가 500 을 냈다.)*

**2. 검증 없이 200 을 주지 않는다.**
`verify`/`settle` 을 안 넘기면 `501` 이 나온다. 우리가 파는 게 "주장과 실행의 일치"인데
우리가 그걸 어기면 안 된다.

**3. 배송비를 숨긴 spread 를 만들지 않는다.**
`spread()` 는 `vs_market_pct` 와 함께 **`landed_vs_market_pct`(배송비 포함)** 를 반드시 낸다.
배송비를 빼고 "싸다"고 말하는 게 이 업계의 흔한 속임수다.

**4. 시세는 출처와 함께 낸다.**
*2026-09-25: 판매자가 자기신고한 시세 두 개가 둘 다 틀렸다. 한쪽은 높게, 한쪽은 낮게 —
**악의가 아니라 몰라서**였다. 그래서 `method` 를 응답에 같이 실어 구매자가 대조하게 한다.*
`seller_reported` 와 `third_party_feed` 는 다른 물건이고, 그 차이를 감추면 안 된다.

**5. 같은 nonce 로 두 번 팔지 않는다.**
에이전트는 재시도한다. 체인이 nonce 재사용을 막아주긴 하지만, 그걸 믿고 안 막으면
**체인에 닿기 전 단계에서** 중복 주문이 생긴다. 같은 nonce 면 같은 주문을 돌려준다.

### 실물은 응답이 아니라 주문이다

x402 는 "결제하면 그 요청의 응답을 준다"는 모델이다. 실물에는 안 맞는다.
그래서 `200` 은 물건이 아니라 **주문번호**를 준다.

```json
{ "order": { "order_id": "ord_…", "transaction": "0x…",
             "status": "awaiting_shipping_address" },
  "next": { "action": "submit_shipping_address", "url": "/orders/ord_…/ship" } }
```

주소는 개인정보라 **온체인에 못 올린다.** 오프체인으로 받아 **주문번호로만** 온체인 결제와 잇는다.
그리고 주소는 API 조회 응답에 **절대 다시 나오지 않는다** — 제출한 사람만 본다.
이 이음매는 x402 스펙에 없다. 우리가 정한 부분이다.

---

## 레일을 새로 넣을 때

`eip712_name`/`eip712_version` 을 **추측하지 마라.** 틀리면 서명이 복구되지 않고,
원인이 "서명자 불일치"로만 보여서 디버깅이 지옥이 된다.

온체인 `DOMAIN_SEPARATOR()` 를 읽어 재구성값과 대조하는 절차를 거쳐라.

```python
from eth_utils import keccak
from eth_abi import encode
TH = keccak(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
calc = keccak(encode(["bytes32","bytes32","bytes32","uint256","address"],
      [TH, keccak(name.encode()), keccak(version.encode()), chain_id, bytes.fromhex(asset[2:])]))
assert "0x"+calc.hex() == onchain_domain_separator
```

현재 대조를 마친 자산:

| 네트워크 | 자산 | name / version | 비고 |
|---|---|---|---|
| `eip155:4663` (Robinhood Chain) | USDG `0x5fc5…1d168` | `Global Dollar` / `1` | 그 체인엔 공개 퍼실리테이터가 없다 |
| `eip155:8453` (Base) | USDC `0x8335…2913` | `USD Coin` / `2` | |

---

## 아직 안 하는 것

정직하게 적어둔다.

- **에스크로가 없다.** 지금은 즉시 이전이라 물건이 안 오면 구매자가 진다.
  x402 v2 스펙에 `escrow` 결제 흐름 자리는 있지만 구현이 없다.
- **판정이 없다.** 판매자가 신고한 시세가 맞았는지, 에이전트가 최선을 골랐는지를
  제3자가 확인하는 층 — 그게 우리가 만들려는 것이고 아직 없다.
- **Node 판이 없다.** 서명이 로컬에서 일어나야 해서 파이썬으로 먼저 냈다.

MIT
