koreafilings 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ *.pyc
5
+ .pytest_cache/
6
+ dist/
7
+ build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 korea-filings-api
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: koreafilings
3
+ Version: 0.1.0
4
+ Summary: Python SDK for koreafilings.com — AI-summarized Korean DART disclosures, paid per call in USDC via x402.
5
+ Project-URL: Homepage, https://koreafilings.com
6
+ Project-URL: Documentation, https://koreafilings.com
7
+ Project-URL: Source, https://github.com/OldTemple91/korea-filings-api
8
+ Project-URL: Issues, https://github.com/OldTemple91/korea-filings-api/issues
9
+ Author: korea-filings-api
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: dart,disclosures,fintech,korea,llm,usdc,x402
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Office/Business :: Financial
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: eth-account<0.14,>=0.13
28
+ Requires-Dist: httpx<1.0,>=0.27
29
+ Requires-Dist: pydantic<3.0,>=2.6
30
+ Description-Content-Type: text/markdown
31
+
32
+ # koreafilings
33
+
34
+ Python SDK for [koreafilings.com](https://koreafilings.com) — AI-summarized
35
+ Korean DART (금융감독원 전자공시) corporate disclosures, paid per call in USDC
36
+ on Base via the [x402](https://www.x402.org/) payment protocol.
37
+
38
+ **No API keys. No monthly fee. No signup.** The wallet that signs the
39
+ payment *is* the identity. First call for a disclosure triggers an LLM
40
+ run; every call after costs the same flat fee from the server-side cache.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install koreafilings
46
+ ```
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ from koreafilings import Client
52
+
53
+ with Client(private_key="0x...", network="base-sepolia") as client:
54
+ summary = client.get_summary("20260424900874")
55
+
56
+ print(f"[{summary.importance_score}/10] {summary.event_type}")
57
+ print(summary.summary_en)
58
+ print("tickers:", summary.ticker_tags)
59
+ print("paid:", client.last_settlement.tx_hash)
60
+ ```
61
+
62
+ Example output:
63
+
64
+ ```
65
+ [7/10] SINGLE_STOCK_TRADING_SUSPENSION
66
+ Global SM's common stock was suspended from trading by the Korea Exchange
67
+ following irregular trading patterns observed on April 23, 2026...
68
+ tickers: ['001680.KS']
69
+ paid: 0x1dbf9885194261e1ad64be8205897ef9c5335ad254921b337c343c23d34b0a72
70
+ ```
71
+
72
+ ## What you get
73
+
74
+ Every response is a structured `Summary` with:
75
+
76
+ | field | type | description |
77
+ |--------------------|-----------------------|---------------------------------------------------|
78
+ | `rcpt_no` | `str` | DART receipt number |
79
+ | `summary_en` | `str` | Paraphrased English summary (never verbatim) |
80
+ | `importance_score` | `int` (1–10) | 10 = M&A / insolvency, 1 = routine admin |
81
+ | `event_type` | `str` | Canonical event taxonomy |
82
+ | `sector_tags` | `list[str]` | GICS-style sector labels |
83
+ | `ticker_tags` | `list[str]` | Affected KRX tickers (`.KS` / `.KQ`) |
84
+ | `actionable_for` | `list[str]` | Audience hints (`QUANT`, `M&A`, …) |
85
+ | `generated_at` | `datetime` | When the summary was produced |
86
+
87
+ ## Pricing
88
+
89
+ `0.005` USDC per summary call, settled on Base. Call `client.get_pricing()`
90
+ for the live machine-readable pricing descriptor including the current
91
+ recipient wallet and USDC contract address.
92
+
93
+ ## Getting a wallet and USDC
94
+
95
+ For testing on Base Sepolia (no real money):
96
+
97
+ 1. Create a test wallet (MetaMask → new account → export private key).
98
+ 2. Get free Base Sepolia ETH from a faucet.
99
+ 3. Get free test USDC from the [Circle faucet](https://faucet.circle.com/).
100
+ 4. Pass the 0x-prefixed private key to `Client(private_key=...)`.
101
+
102
+ For production use on Base mainnet, fund a wallet with real USDC and
103
+ pass `network="base"` instead.
104
+
105
+ ## How payment works
106
+
107
+ Under the hood, each paid call:
108
+
109
+ 1. GET the endpoint without payment → server returns **402** with a
110
+ [x402 `accepts`](https://www.x402.org/) block describing exact
111
+ amount, USDC contract, network, recipient, and expiry.
112
+ 2. SDK signs an
113
+ [EIP-3009 `TransferWithAuthorization`](https://eips.ethereum.org/EIPS/eip-3009)
114
+ message locally (your private key never leaves the process).
115
+ 3. SDK base64-encodes the signed payload into `X-PAYMENT` and retries
116
+ the GET.
117
+ 4. Server verifies the signature via the x402 facilitator, submits it
118
+ on-chain, streams the JSON body back to you, and attaches the
119
+ settlement proof as `X-PAYMENT-RESPONSE`.
120
+
121
+ The SDK exposes the proof as `client.last_settlement` so you can log
122
+ transaction hashes for your accounting system.
123
+
124
+ ## Error handling
125
+
126
+ ```python
127
+ from koreafilings import Client, ApiError, PaymentError, ConfigurationError
128
+
129
+ try:
130
+ summary = client.get_summary("20260424900874")
131
+ except PaymentError as e:
132
+ # Facilitator rejected the signed payment (bad sig, low balance,
133
+ # expired auth, network mismatch).
134
+ print("payment failed:", e.reason, e.detail)
135
+ except ApiError as e:
136
+ # Non-payment HTTP failure: 404 unknown rcpt_no, 429 rate limit,
137
+ # 5xx upstream outage.
138
+ print("api error:", e.status_code, e.body)
139
+ except ConfigurationError as e:
140
+ # Bad private_key format or unknown network alias.
141
+ print("config:", e)
142
+ ```
143
+
144
+ ## Security notes
145
+
146
+ - **The private key signs real-money authorizations.** Do not ship it
147
+ in client-side code, do not put it in a `.env` checked into git, do
148
+ not paste it in chat. Prefer a dedicated burner wallet funded with
149
+ only the USDC you plan to spend.
150
+ - The SDK signs locally; the key is never transmitted to koreafilings.com
151
+ or to the facilitator.
152
+ - Every payment carries an `EIP-3009` nonce. The facilitator refuses
153
+ replays.
154
+ - For Base Sepolia testing only, a fresh wallet with faucet funds is
155
+ the safest pattern — nothing on that wallet has production value.
156
+
157
+ ## Source & feedback
158
+
159
+ - Repo: <https://github.com/OldTemple91/korea-filings-api>
160
+ - Issues: <https://github.com/OldTemple91/korea-filings-api/issues>
161
+ - Landing: <https://koreafilings.com>
162
+
163
+ MIT-licensed.
@@ -0,0 +1,132 @@
1
+ # koreafilings
2
+
3
+ Python SDK for [koreafilings.com](https://koreafilings.com) — AI-summarized
4
+ Korean DART (금융감독원 전자공시) corporate disclosures, paid per call in USDC
5
+ on Base via the [x402](https://www.x402.org/) payment protocol.
6
+
7
+ **No API keys. No monthly fee. No signup.** The wallet that signs the
8
+ payment *is* the identity. First call for a disclosure triggers an LLM
9
+ run; every call after costs the same flat fee from the server-side cache.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install koreafilings
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ from koreafilings import Client
21
+
22
+ with Client(private_key="0x...", network="base-sepolia") as client:
23
+ summary = client.get_summary("20260424900874")
24
+
25
+ print(f"[{summary.importance_score}/10] {summary.event_type}")
26
+ print(summary.summary_en)
27
+ print("tickers:", summary.ticker_tags)
28
+ print("paid:", client.last_settlement.tx_hash)
29
+ ```
30
+
31
+ Example output:
32
+
33
+ ```
34
+ [7/10] SINGLE_STOCK_TRADING_SUSPENSION
35
+ Global SM's common stock was suspended from trading by the Korea Exchange
36
+ following irregular trading patterns observed on April 23, 2026...
37
+ tickers: ['001680.KS']
38
+ paid: 0x1dbf9885194261e1ad64be8205897ef9c5335ad254921b337c343c23d34b0a72
39
+ ```
40
+
41
+ ## What you get
42
+
43
+ Every response is a structured `Summary` with:
44
+
45
+ | field | type | description |
46
+ |--------------------|-----------------------|---------------------------------------------------|
47
+ | `rcpt_no` | `str` | DART receipt number |
48
+ | `summary_en` | `str` | Paraphrased English summary (never verbatim) |
49
+ | `importance_score` | `int` (1–10) | 10 = M&A / insolvency, 1 = routine admin |
50
+ | `event_type` | `str` | Canonical event taxonomy |
51
+ | `sector_tags` | `list[str]` | GICS-style sector labels |
52
+ | `ticker_tags` | `list[str]` | Affected KRX tickers (`.KS` / `.KQ`) |
53
+ | `actionable_for` | `list[str]` | Audience hints (`QUANT`, `M&A`, …) |
54
+ | `generated_at` | `datetime` | When the summary was produced |
55
+
56
+ ## Pricing
57
+
58
+ `0.005` USDC per summary call, settled on Base. Call `client.get_pricing()`
59
+ for the live machine-readable pricing descriptor including the current
60
+ recipient wallet and USDC contract address.
61
+
62
+ ## Getting a wallet and USDC
63
+
64
+ For testing on Base Sepolia (no real money):
65
+
66
+ 1. Create a test wallet (MetaMask → new account → export private key).
67
+ 2. Get free Base Sepolia ETH from a faucet.
68
+ 3. Get free test USDC from the [Circle faucet](https://faucet.circle.com/).
69
+ 4. Pass the 0x-prefixed private key to `Client(private_key=...)`.
70
+
71
+ For production use on Base mainnet, fund a wallet with real USDC and
72
+ pass `network="base"` instead.
73
+
74
+ ## How payment works
75
+
76
+ Under the hood, each paid call:
77
+
78
+ 1. GET the endpoint without payment → server returns **402** with a
79
+ [x402 `accepts`](https://www.x402.org/) block describing exact
80
+ amount, USDC contract, network, recipient, and expiry.
81
+ 2. SDK signs an
82
+ [EIP-3009 `TransferWithAuthorization`](https://eips.ethereum.org/EIPS/eip-3009)
83
+ message locally (your private key never leaves the process).
84
+ 3. SDK base64-encodes the signed payload into `X-PAYMENT` and retries
85
+ the GET.
86
+ 4. Server verifies the signature via the x402 facilitator, submits it
87
+ on-chain, streams the JSON body back to you, and attaches the
88
+ settlement proof as `X-PAYMENT-RESPONSE`.
89
+
90
+ The SDK exposes the proof as `client.last_settlement` so you can log
91
+ transaction hashes for your accounting system.
92
+
93
+ ## Error handling
94
+
95
+ ```python
96
+ from koreafilings import Client, ApiError, PaymentError, ConfigurationError
97
+
98
+ try:
99
+ summary = client.get_summary("20260424900874")
100
+ except PaymentError as e:
101
+ # Facilitator rejected the signed payment (bad sig, low balance,
102
+ # expired auth, network mismatch).
103
+ print("payment failed:", e.reason, e.detail)
104
+ except ApiError as e:
105
+ # Non-payment HTTP failure: 404 unknown rcpt_no, 429 rate limit,
106
+ # 5xx upstream outage.
107
+ print("api error:", e.status_code, e.body)
108
+ except ConfigurationError as e:
109
+ # Bad private_key format or unknown network alias.
110
+ print("config:", e)
111
+ ```
112
+
113
+ ## Security notes
114
+
115
+ - **The private key signs real-money authorizations.** Do not ship it
116
+ in client-side code, do not put it in a `.env` checked into git, do
117
+ not paste it in chat. Prefer a dedicated burner wallet funded with
118
+ only the USDC you plan to spend.
119
+ - The SDK signs locally; the key is never transmitted to koreafilings.com
120
+ or to the facilitator.
121
+ - Every payment carries an `EIP-3009` nonce. The facilitator refuses
122
+ replays.
123
+ - For Base Sepolia testing only, a fresh wallet with faucet funds is
124
+ the safest pattern — nothing on that wallet has production value.
125
+
126
+ ## Source & feedback
127
+
128
+ - Repo: <https://github.com/OldTemple91/korea-filings-api>
129
+ - Issues: <https://github.com/OldTemple91/korea-filings-api/issues>
130
+ - Landing: <https://koreafilings.com>
131
+
132
+ MIT-licensed.
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "koreafilings"
7
+ version = "0.1.0"
8
+ description = "Python SDK for koreafilings.com — AI-summarized Korean DART disclosures, paid per call in USDC via x402."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "korea-filings-api" }]
13
+ keywords = ["dart", "korea", "disclosures", "x402", "llm", "fintech", "usdc"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Office/Business :: Financial",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "httpx>=0.27,<1.0",
31
+ "pydantic>=2.6,<3.0",
32
+ "eth-account>=0.13,<0.14",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://koreafilings.com"
37
+ Documentation = "https://koreafilings.com"
38
+ Source = "https://github.com/OldTemple91/korea-filings-api"
39
+ Issues = "https://github.com/OldTemple91/korea-filings-api/issues"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/koreafilings"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = ["/src", "/README.md", "/LICENSE", "/pyproject.toml"]
@@ -0,0 +1,29 @@
1
+ """koreafilings — Python SDK for AI-summarized Korean DART disclosures.
2
+
3
+ Minimal usage:
4
+
5
+ from koreafilings import Client
6
+
7
+ with Client(private_key="0x...", network="base-sepolia") as client:
8
+ summary = client.get_summary("20260424900874")
9
+ print(summary.importance_score, summary.summary_en)
10
+ print("paid tx:", client.last_settlement.tx_hash)
11
+ """
12
+
13
+ from .client import Client
14
+ from .errors import ApiError, ConfigurationError, KoreaFilingsError, PaymentError
15
+ from .models import Pricing, PricingEndpoint, SettlementProof, Summary
16
+
17
+ __all__ = [
18
+ "Client",
19
+ "Summary",
20
+ "SettlementProof",
21
+ "Pricing",
22
+ "PricingEndpoint",
23
+ "KoreaFilingsError",
24
+ "ApiError",
25
+ "PaymentError",
26
+ "ConfigurationError",
27
+ ]
28
+
29
+ __version__ = "0.1.0"
@@ -0,0 +1,141 @@
1
+ """x402 payment internals.
2
+
3
+ This module is intentionally underscore-prefixed. It is not part of the
4
+ public SDK surface — callers should reach it only via the public
5
+ ``Client`` class.
6
+
7
+ What lives here:
8
+
9
+ - parsing the 402 "accepts" block the API returns,
10
+ - building the EIP-3009 TransferWithAuthorization message,
11
+ - signing it with the caller's private key (EIP-712),
12
+ - serialising the signed ``PaymentPayload`` as the base64 ``X-PAYMENT``
13
+ header the x402 protocol expects.
14
+
15
+ We deliberately do not talk to the facilitator from the client. The
16
+ server's interceptor submits the signed payment to the facilitator,
17
+ which keeps the SDK small and avoids leaking facilitator URL choices
18
+ into user code.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ import json
25
+ import secrets
26
+ import time
27
+ from typing import Any, Mapping
28
+
29
+ from eth_account import Account
30
+ from eth_account.messages import encode_typed_data
31
+
32
+ from .errors import PaymentError
33
+
34
+
35
+ X402_VERSION = 2
36
+
37
+
38
+ def select_requirement(accepts: list[Mapping[str, Any]]) -> Mapping[str, Any]:
39
+ """Pick the first payment requirement the server offers.
40
+
41
+ The 402 body carries a list of acceptable payment schemes. Today the
42
+ server only ever offers one (USDC on whatever network it's configured
43
+ for), so we take index 0. If the server starts offering multiple
44
+ schemes, this is the place to prefer the cheaper one.
45
+ """
46
+ if not accepts:
47
+ raise PaymentError(reason="empty_accepts", detail="server returned 402 with no payment options")
48
+ return accepts[0]
49
+
50
+
51
+ def build_authorization(payer_address: str, requirement: Mapping[str, Any]) -> dict:
52
+ """Assemble the EIP-3009 authorization fields.
53
+
54
+ ``validAfter`` gets a 10-second backfill so a payer clock that is
55
+ slightly behind the facilitator's doesn't cause a rejection.
56
+ ``validBefore`` uses the ``maxTimeoutSeconds`` the server advertised.
57
+ ``nonce`` is random 32 bytes — the facilitator rejects replays.
58
+ """
59
+ now = int(time.time())
60
+ return {
61
+ "from": payer_address,
62
+ "to": requirement["payTo"],
63
+ "value": requirement["amount"],
64
+ "validAfter": str(max(0, now - 10)),
65
+ "validBefore": str(now + int(requirement["maxTimeoutSeconds"])),
66
+ "nonce": "0x" + secrets.token_hex(32),
67
+ }
68
+
69
+
70
+ def sign_eip3009(account: Account, requirement: Mapping[str, Any], authorization: Mapping[str, Any]) -> str:
71
+ """Produce the 65-byte EIP-712 signature over the authorization."""
72
+ chain_id = int(requirement["network"].split(":")[1])
73
+ extra = requirement.get("extra") or {}
74
+ typed = {
75
+ "types": {
76
+ "EIP712Domain": [
77
+ {"name": "name", "type": "string"},
78
+ {"name": "version", "type": "string"},
79
+ {"name": "chainId", "type": "uint256"},
80
+ {"name": "verifyingContract", "type": "address"},
81
+ ],
82
+ "TransferWithAuthorization": [
83
+ {"name": "from", "type": "address"},
84
+ {"name": "to", "type": "address"},
85
+ {"name": "value", "type": "uint256"},
86
+ {"name": "validAfter", "type": "uint256"},
87
+ {"name": "validBefore", "type": "uint256"},
88
+ {"name": "nonce", "type": "bytes32"},
89
+ ],
90
+ },
91
+ "primaryType": "TransferWithAuthorization",
92
+ "domain": {
93
+ "name": extra.get("name", "USDC"),
94
+ "version": extra.get("version", "2"),
95
+ "chainId": chain_id,
96
+ "verifyingContract": requirement["asset"],
97
+ },
98
+ "message": {
99
+ "from": authorization["from"],
100
+ "to": authorization["to"],
101
+ "value": int(authorization["value"]),
102
+ "validAfter": int(authorization["validAfter"]),
103
+ "validBefore": int(authorization["validBefore"]),
104
+ "nonce": authorization["nonce"],
105
+ },
106
+ }
107
+ signable = encode_typed_data(full_message=typed)
108
+ signed = account.sign_message(signable)
109
+ hex_sig = signed.signature.hex()
110
+ return hex_sig if hex_sig.startswith("0x") else f"0x{hex_sig}"
111
+
112
+
113
+ def build_x_payment_header(
114
+ resource_url: str,
115
+ requirement: Mapping[str, Any],
116
+ authorization: Mapping[str, Any],
117
+ signature: str,
118
+ ) -> str:
119
+ """Base64-encode the signed payload into the ``X-PAYMENT`` header value."""
120
+ payload = {
121
+ "x402Version": X402_VERSION,
122
+ "resource": {
123
+ "url": resource_url,
124
+ "description": requirement.get("description", ""),
125
+ "mimeType": "application/json",
126
+ },
127
+ "accepted": requirement,
128
+ "payload": {"signature": signature, "authorization": authorization},
129
+ }
130
+ raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
131
+ return base64.b64encode(raw).decode("ascii")
132
+
133
+
134
+ def decode_settlement_header(value: str | None) -> dict | None:
135
+ """Decode the ``X-PAYMENT-RESPONSE`` settlement proof, or return None."""
136
+ if not value:
137
+ return None
138
+ try:
139
+ return json.loads(base64.b64decode(value))
140
+ except Exception: # noqa: BLE001 — any decode failure returns None
141
+ return None
@@ -0,0 +1,183 @@
1
+ """Public client for koreafilings.com.
2
+
3
+ The only class callers need:
4
+
5
+ from koreafilings import Client
6
+
7
+ client = Client(private_key="0x...", network="base-sepolia")
8
+ summary = client.get_summary("20260424900874")
9
+
10
+ The client hides the full x402 flow: it issues the GET, detects the
11
+ 402 payment prompt, signs an EIP-3009 ``TransferWithAuthorization``,
12
+ resubmits the request with the ``X-PAYMENT`` header, and parses the
13
+ 200 body. Settlement proofs from ``X-PAYMENT-RESPONSE`` are attached to
14
+ the returned model via the ``last_settlement`` property.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ import httpx
22
+ from eth_account import Account
23
+
24
+ from . import _payment
25
+ from .errors import ApiError, ConfigurationError, PaymentError
26
+ from .models import Pricing, SettlementProof, Summary
27
+
28
+
29
+ DEFAULT_BASE_URL = "https://api.koreafilings.com"
30
+ DEFAULT_TIMEOUT = 60.0
31
+
32
+ # Maps the short network aliases callers are allowed to pass into the
33
+ # constructor to the CAIP-2 identifiers the server uses in its 402
34
+ # responses. We only use this for up-front validation — the actual
35
+ # network is always whatever the server's 402 body says, so that callers
36
+ # never accidentally sign for a different chain than the server expects.
37
+ _NETWORK_ALIASES = {
38
+ "base-sepolia": "eip155:84532",
39
+ "base": "eip155:8453",
40
+ "base-mainnet": "eip155:8453",
41
+ }
42
+
43
+
44
+ class Client:
45
+ """Blocking HTTP client for the koreafilings paid API.
46
+
47
+ Parameters
48
+ ----------
49
+ private_key:
50
+ A 0x-prefixed 32-byte hex string. This key signs the EIP-3009
51
+ authorizations that move USDC from your wallet to the API's
52
+ recipient wallet. **Keep this secret.** The SDK never transmits
53
+ it — signing happens locally, and only the signature goes on the
54
+ wire.
55
+ network:
56
+ Short alias (``"base-sepolia"`` or ``"base"``) used only for
57
+ sanity checks. The authoritative network comes from the 402
58
+ response the server sends; if it disagrees with ``network``,
59
+ ``get_summary`` raises :class:`PaymentError` before signing.
60
+ base_url:
61
+ Override for self-hosted deployments or local development.
62
+ Defaults to ``https://api.koreafilings.com``.
63
+ timeout:
64
+ Per-request timeout in seconds. The paid summary call can take
65
+ up to ~10s on a cold disclosure (LLM latency), so the default
66
+ is generous.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ private_key: str,
72
+ network: str = "base-sepolia",
73
+ base_url: str = DEFAULT_BASE_URL,
74
+ timeout: float = DEFAULT_TIMEOUT,
75
+ ):
76
+ if not private_key.startswith("0x") or len(private_key) != 66:
77
+ raise ConfigurationError("private_key must be a 0x-prefixed 32-byte hex string (66 chars)")
78
+ if network not in _NETWORK_ALIASES:
79
+ raise ConfigurationError(
80
+ f"unknown network '{network}'; expected one of {sorted(_NETWORK_ALIASES)}"
81
+ )
82
+ self._account = Account.from_key(private_key)
83
+ self._expected_chain = _NETWORK_ALIASES[network]
84
+ self._base_url = base_url.rstrip("/")
85
+ self._http = httpx.Client(timeout=timeout)
86
+ self._last_settlement: Optional[SettlementProof] = None
87
+
88
+ # ------------------------------------------------------------------
89
+ # Public API
90
+ # ------------------------------------------------------------------
91
+
92
+ @property
93
+ def address(self) -> str:
94
+ """Payer wallet address derived from the configured private key."""
95
+ return self._account.address
96
+
97
+ @property
98
+ def last_settlement(self) -> Optional[SettlementProof]:
99
+ """Settlement proof from the most recent successful paid call, if any."""
100
+ return self._last_settlement
101
+
102
+ def get_pricing(self) -> Pricing:
103
+ """Fetch the public pricing descriptor. No payment required."""
104
+ resp = self._http.get(f"{self._base_url}/v1/pricing")
105
+ if resp.status_code != 200:
106
+ raise ApiError(resp.status_code, _safe_json(resp))
107
+ return Pricing.model_validate(resp.json())
108
+
109
+ def get_summary(self, rcpt_no: str) -> Summary:
110
+ """Fetch the AI summary for a DART receipt number, paying if required.
111
+
112
+ The first call for a given ``rcpt_no`` triggers an LLM run on
113
+ the server and costs USDC (0.005 as of v0.1). Every subsequent
114
+ call for the same disclosure is served from the server-side
115
+ cache and still costs the same fee — the cache improves server
116
+ margins, not caller price. Errors map to:
117
+
118
+ - :class:`ApiError` for HTTP failures that aren't payment prompts,
119
+ - :class:`PaymentError` if signing/settlement is rejected.
120
+ """
121
+ url = f"{self._base_url}/v1/disclosures/{rcpt_no}/summary"
122
+
123
+ unpaid = self._http.get(url)
124
+ if unpaid.status_code == 200:
125
+ # Free/cached response path (the server currently always charges,
126
+ # but a future free-tier could land here without breaking callers).
127
+ self._last_settlement = None
128
+ return Summary.model_validate(unpaid.json())
129
+ if unpaid.status_code != 402:
130
+ raise ApiError(unpaid.status_code, _safe_json(unpaid))
131
+
132
+ body = _safe_json(unpaid) or {}
133
+ requirement = _payment.select_requirement(body.get("accepts") or [])
134
+
135
+ advertised = requirement.get("network")
136
+ if advertised != self._expected_chain:
137
+ raise PaymentError(
138
+ reason="network_mismatch",
139
+ detail=f"server advertises {advertised}, client configured for {self._expected_chain}",
140
+ )
141
+
142
+ authorization = _payment.build_authorization(self._account.address, requirement)
143
+ signature = _payment.sign_eip3009(self._account, requirement, authorization)
144
+ header_value = _payment.build_x_payment_header(url, requirement, authorization, signature)
145
+
146
+ paid = self._http.get(url, headers={"X-PAYMENT": header_value})
147
+ if paid.status_code == 402:
148
+ # Facilitator rejected after we signed — bubble up its reason.
149
+ rejection = _safe_json(paid) or {}
150
+ raise PaymentError(
151
+ reason=rejection.get("error") or "payment_rejected",
152
+ detail=rejection,
153
+ )
154
+ if paid.status_code != 200:
155
+ raise ApiError(paid.status_code, _safe_json(paid))
156
+
157
+ settlement_raw = _payment.decode_settlement_header(paid.headers.get("X-PAYMENT-RESPONSE"))
158
+ self._last_settlement = (
159
+ SettlementProof.model_validate(settlement_raw) if settlement_raw else None
160
+ )
161
+ return Summary.model_validate(paid.json())
162
+
163
+ def close(self) -> None:
164
+ """Close the underlying HTTP connection pool."""
165
+ self._http.close()
166
+
167
+ # ------------------------------------------------------------------
168
+ # Context manager support
169
+ # ------------------------------------------------------------------
170
+
171
+ def __enter__(self) -> "Client":
172
+ return self
173
+
174
+ def __exit__(self, exc_type, exc, tb) -> None:
175
+ self.close()
176
+
177
+
178
+ def _safe_json(resp: httpx.Response) -> Optional[dict]:
179
+ """Best-effort JSON parse; returns None on any failure."""
180
+ try:
181
+ return resp.json()
182
+ except Exception: # noqa: BLE001
183
+ return None
@@ -0,0 +1,55 @@
1
+ """Exception hierarchy for the koreafilings SDK.
2
+
3
+ Callers catch ``KoreaFilingsError`` to handle any SDK-level failure, or
4
+ a more specific subclass when they care to distinguish payment issues
5
+ from transport issues.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Mapping
11
+
12
+
13
+ class KoreaFilingsError(Exception):
14
+ """Base class for every error this SDK raises."""
15
+
16
+
17
+ class ApiError(KoreaFilingsError):
18
+ """The API returned a non-2xx response that is not a 402 payment prompt.
19
+
20
+ Typical causes: 404 (unknown rcpt_no), 429 (rate limit), 5xx (upstream
21
+ DART or LLM transient failure). ``status_code`` is the HTTP status;
22
+ ``body`` is the raw JSON body if the server supplied one, else ``None``.
23
+ """
24
+
25
+ def __init__(self, status_code: int, body: Any | None, message: str | None = None):
26
+ self.status_code = status_code
27
+ self.body = body
28
+ suffix = f": {message}" if message else ""
29
+ super().__init__(f"API returned {status_code}{suffix}")
30
+
31
+
32
+ class PaymentError(KoreaFilingsError):
33
+ """The facilitator or the API rejected the x402 payment attempt.
34
+
35
+ Raised when a signed payment is rejected by the facilitator (bad
36
+ signature, insufficient balance, expired authorization) or when the
37
+ API replies 402 a second time after we submitted a payment header.
38
+ ``reason`` is the short code returned by the facilitator when one
39
+ is available.
40
+ """
41
+
42
+ def __init__(self, reason: str | None, detail: Mapping[str, Any] | str | None = None):
43
+ self.reason = reason
44
+ self.detail = detail
45
+ parts = [p for p in (reason, str(detail) if detail else None) if p]
46
+ super().__init__(" — ".join(parts) if parts else "payment rejected")
47
+
48
+
49
+ class ConfigurationError(KoreaFilingsError):
50
+ """The SDK was constructed with invalid arguments.
51
+
52
+ Raised eagerly at construction time so bad configs never reach the
53
+ first HTTP call. Examples: malformed private key, unknown network
54
+ alias.
55
+ """
@@ -0,0 +1,78 @@
1
+ """Response models.
2
+
3
+ Every model uses Pydantic v2 so callers get runtime validation, static
4
+ type-checker hints, and ``.model_dump_json()`` for free. Field names
5
+ stay in the API's camelCase form because that's what the wire returns —
6
+ we do not re-camelcase on the way in.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime
12
+ from typing import List, Optional
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+
17
+ class Summary(BaseModel):
18
+ """AI-generated English summary of a single DART disclosure.
19
+
20
+ This is the payload the paid endpoint returns after a successful
21
+ x402 settlement. ``importance_score`` is 1–10, where 10 is an event
22
+ like M&A or insolvency and 1 is a routine administrative filing.
23
+ """
24
+
25
+ model_config = ConfigDict(populate_by_name=True, frozen=True)
26
+
27
+ rcpt_no: str = Field(alias="rcptNo")
28
+ summary_en: str = Field(alias="summaryEn")
29
+ importance_score: int = Field(alias="importanceScore", ge=1, le=10)
30
+ event_type: str = Field(alias="eventType")
31
+ sector_tags: List[str] = Field(alias="sectorTags", default_factory=list)
32
+ ticker_tags: List[str] = Field(alias="tickerTags", default_factory=list)
33
+ actionable_for: List[str] = Field(alias="actionableFor", default_factory=list)
34
+ generated_at: datetime = Field(alias="generatedAt")
35
+
36
+
37
+ class SettlementProof(BaseModel):
38
+ """On-chain settlement proof the API returns via ``X-PAYMENT-RESPONSE``.
39
+
40
+ ``tx_hash`` is the Base transaction hash you can look up on
41
+ basescan.org. ``network`` is the CAIP-2 network identifier (e.g.
42
+ ``eip155:84532`` for Base Sepolia, ``eip155:8453`` for Base mainnet).
43
+ ``error_reason`` is populated only when ``success`` is ``False`` —
44
+ the facilitator normally only reaches this path on partial
45
+ settlements, since hard rejects surface as ``PaymentError`` before
46
+ a proof is emitted.
47
+ """
48
+
49
+ model_config = ConfigDict(populate_by_name=True, frozen=True)
50
+
51
+ success: bool
52
+ tx_hash: Optional[str] = Field(alias="transaction", default=None)
53
+ network: Optional[str] = None
54
+ payer: Optional[str] = None
55
+ error_reason: Optional[str] = Field(alias="errorReason", default=None)
56
+
57
+
58
+ class Pricing(BaseModel):
59
+ """Machine-readable pricing descriptor from ``GET /v1/pricing``."""
60
+
61
+ model_config = ConfigDict(populate_by_name=True, frozen=True)
62
+
63
+ x402_network: str = Field(alias="x402Network")
64
+ x402_asset: str = Field(alias="x402Asset")
65
+ x402_recipient: str = Field(alias="x402Recipient")
66
+ endpoints: List["PricingEndpoint"]
67
+
68
+
69
+ class PricingEndpoint(BaseModel):
70
+ model_config = ConfigDict(populate_by_name=True, frozen=True)
71
+
72
+ method: str
73
+ path: str
74
+ price_usdc: str = Field(alias="priceUsdc")
75
+ description: str
76
+
77
+
78
+ Pricing.model_rebuild()