ondaru 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.
Files changed (79) hide show
  1. ondaru-0.1.0/.env.example +39 -0
  2. ondaru-0.1.0/.gitattributes +2 -0
  3. ondaru-0.1.0/.gitignore +29 -0
  4. ondaru-0.1.0/CLAUDE.md +364 -0
  5. ondaru-0.1.0/Dockerfile +32 -0
  6. ondaru-0.1.0/Makefile +31 -0
  7. ondaru-0.1.0/PKG-INFO +93 -0
  8. ondaru-0.1.0/README.md +65 -0
  9. ondaru-0.1.0/docker-compose.yml +51 -0
  10. ondaru-0.1.0/docs/going-live.md +89 -0
  11. ondaru-0.1.0/docs/notes/2026-07-28-okx-asp-research.md +380 -0
  12. ondaru-0.1.0/docs/notes/pitch-draft.md +248 -0
  13. ondaru-0.1.0/docs/notes/sources.md +290 -0
  14. ondaru-0.1.0/docs/plans/2026-07-28-001-feat-agent-3d-product-mockups-plan.md +518 -0
  15. ondaru-0.1.0/docs/proof-of-payment.md +142 -0
  16. ondaru-0.1.0/fakes/generation-backend/Dockerfile +13 -0
  17. ondaru-0.1.0/fakes/generation-backend/app.py +169 -0
  18. ondaru-0.1.0/pyproject.toml +94 -0
  19. ondaru-0.1.0/railway.json +21 -0
  20. ondaru-0.1.0/src/ondaru/__init__.py +3 -0
  21. ondaru-0.1.0/src/ondaru/cli/__init__.py +1 -0
  22. ondaru-0.1.0/src/ondaru/cli/main.py +593 -0
  23. ondaru-0.1.0/src/ondaru/config.py +259 -0
  24. ondaru-0.1.0/src/ondaru/contracts.py +338 -0
  25. ondaru-0.1.0/src/ondaru/generation/__init__.py +1 -0
  26. ondaru-0.1.0/src/ondaru/generation/adapter.py +51 -0
  27. ondaru-0.1.0/src/ondaru/generation/factory.py +27 -0
  28. ondaru-0.1.0/src/ondaru/generation/tripo.py +274 -0
  29. ondaru-0.1.0/src/ondaru/generation/vendor.py +154 -0
  30. ondaru-0.1.0/src/ondaru/mcp/__init__.py +1 -0
  31. ondaru-0.1.0/src/ondaru/mcp/__main__.py +182 -0
  32. ondaru-0.1.0/src/ondaru/mcp/payments.py +276 -0
  33. ondaru-0.1.0/src/ondaru/mcp/paywall.py +336 -0
  34. ondaru-0.1.0/src/ondaru/mcp/server.py +299 -0
  35. ondaru-0.1.0/src/ondaru/mcp/verification.py +320 -0
  36. ondaru-0.1.0/src/ondaru/payments/__init__.py +1 -0
  37. ondaru-0.1.0/src/ondaru/payments/balances.py +85 -0
  38. ondaru-0.1.0/src/ondaru/payments/domain.py +123 -0
  39. ondaru-0.1.0/src/ondaru/payments/settlement.py +115 -0
  40. ondaru-0.1.0/src/ondaru/payments/settler.py +343 -0
  41. ondaru-0.1.0/src/ondaru/server/__init__.py +1 -0
  42. ondaru-0.1.0/src/ondaru/server/__main__.py +43 -0
  43. ondaru-0.1.0/src/ondaru/server/http_app.py +109 -0
  44. ondaru-0.1.0/src/ondaru/skill_assets/ondaru.md +70 -0
  45. ondaru-0.1.0/src/ondaru/spec/__init__.py +1 -0
  46. ondaru-0.1.0/src/ondaru/spec/builder.py +85 -0
  47. ondaru-0.1.0/src/ondaru/storage/__init__.py +1 -0
  48. ondaru-0.1.0/src/ondaru/storage/assets.py +94 -0
  49. ondaru-0.1.0/src/ondaru/store/__init__.py +1 -0
  50. ondaru-0.1.0/src/ondaru/store/postgres.py +412 -0
  51. ondaru-0.1.0/src/ondaru/store/redis.py +115 -0
  52. ondaru-0.1.0/src/ondaru/store/schema.sql +81 -0
  53. ondaru-0.1.0/src/ondaru/worker/pipeline.py +187 -0
  54. ondaru-0.1.0/tests/__init__.py +0 -0
  55. ondaru-0.1.0/tests/integrations/__init__.py +0 -0
  56. ondaru-0.1.0/tests/integrations/_payment_probe_server.py +55 -0
  57. ondaru-0.1.0/tests/integrations/conftest.py +87 -0
  58. ondaru-0.1.0/tests/integrations/test_balance_refusal.py +141 -0
  59. ondaru-0.1.0/tests/integrations/test_business_flows.py +376 -0
  60. ondaru-0.1.0/tests/integrations/test_cache_and_limits.py +121 -0
  61. ondaru-0.1.0/tests/integrations/test_cli.py +118 -0
  62. ondaru-0.1.0/tests/integrations/test_domain_guard.py +160 -0
  63. ondaru-0.1.0/tests/integrations/test_fake_generation_backend.py +120 -0
  64. ondaru-0.1.0/tests/integrations/test_generation_adapter.py +208 -0
  65. ondaru-0.1.0/tests/integrations/test_job_repository.py +198 -0
  66. ondaru-0.1.0/tests/integrations/test_key_rotation.py +223 -0
  67. ondaru-0.1.0/tests/integrations/test_mainnet_guards.py +107 -0
  68. ondaru-0.1.0/tests/integrations/test_mcp_tools.py +327 -0
  69. ondaru-0.1.0/tests/integrations/test_payment_surface.py +131 -0
  70. ondaru-0.1.0/tests/integrations/test_paywall_enforcement.py +132 -0
  71. ondaru-0.1.0/tests/integrations/test_replay_defence.py +297 -0
  72. ondaru-0.1.0/tests/integrations/test_settlement_gate.py +267 -0
  73. ondaru-0.1.0/tests/integrations/test_worker_pipeline.py +235 -0
  74. ondaru-0.1.0/tests/load/order_and_poll.js +107 -0
  75. ondaru-0.1.0/tests/load/paid_flow.js +141 -0
  76. ondaru-0.1.0/tests/load/sign_authorizations.py +113 -0
  77. ondaru-0.1.0/tests/load/spec_tier.js +81 -0
  78. ondaru-0.1.0/tests/load/thresholds.js +79 -0
  79. ondaru-0.1.0/uv.lock +2869 -0
@@ -0,0 +1,39 @@
1
+ # Copy to .env and fill in. Nothing here has a usable default — a missing
2
+ # credential fails at startup rather than running against nothing.
3
+
4
+ # --- Payment ------------------------------------------------------------
5
+ # X Layer. The settlement token is fixed and registered in code; only the
6
+ # recipient is yours. It MUST match the address the ASP is registered against,
7
+ # because a mismatch sends a caller's funds somewhere else.
8
+ ONDARU_PAY_TO=0x0000000000000000000000000000000000000000
9
+ ONDARU_PAYMENT_NETWORK=eip155:196
10
+ ONDARU_PAYMENT_TIMEOUT=120
11
+
12
+ # --- Pricing ------------------------------------------------------------
13
+ # Measured generation cost is $0.19-$0.30 per asset. The floor is enforced at
14
+ # startup: an asset price at or below it refuses to boot rather than selling
15
+ # at a loss. Observed marketplace maximum is 0.96.
16
+ ONDARU_SPEC_PRICE=0.02
17
+ ONDARU_ASSET_PRICE=0.90
18
+ ONDARU_ASSET_PRICE_FLOOR=0.30
19
+ ONDARU_CURRENCY=USDT
20
+
21
+ # --- Generation backend -------------------------------------------------
22
+ ONDARU_GENERATION_BASE_URL=https://api.your-vendor.example
23
+ ONDARU_GENERATION_API_KEY=
24
+ ONDARU_GENERATION_SUBMIT_TIMEOUT=30
25
+ ONDARU_GENERATION_POLL_INTERVAL=5
26
+ ONDARU_GENERATION_POLL_DEADLINE=900
27
+
28
+ # --- Infrastructure -----------------------------------------------------
29
+ ONDARU_DATABASE_URL=postgresql://ondaru:ondaru@127.0.0.1:55432/ondaru_test
30
+ ONDARU_REDIS_URL=redis://127.0.0.1:56379/0
31
+ ONDARU_DB_POOL_MIN=1
32
+ ONDARU_DB_POOL_MAX=10
33
+
34
+ # --- Service ------------------------------------------------------------
35
+ # Asset URLs are built from this, so it must be the address callers can reach.
36
+ ONDARU_PUBLIC_BASE_URL=https://ondaru.example
37
+ ONDARU_RETENTION_HOURS=72
38
+ ONDARU_STATUS_CACHE_TTL=5
39
+ ONDARU_RATE_LIMIT_PER_MINUTE=60
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,29 @@
1
+ # Reference repositories cloned for study — never committed.
2
+ references/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ .venv/
8
+ .venv-*/
9
+ *.egg-info/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .mypy_cache/
13
+
14
+ # Local environment
15
+ .env
16
+ .env.local
17
+
18
+ # Generated assets and every test artefact. `out/` is scratch: generated models,
19
+ # renders, captured payloads, server logs. The evidence the README cites lives
20
+ # in docs/, which is text and reviewable; the binaries it describes do not
21
+ # belong in git.
22
+ assets/
23
+ .artifacts/
24
+ out/
25
+ dist/
26
+
27
+ # Signed payment authorisations for the load test. Each one is a bearer
28
+ # instrument that moves real funds when redeemed.
29
+ tests/load/authorizations.json
ondaru-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,364 @@
1
+ # Ondaru
2
+
3
+ Agent-ordered 3D product mockups, served as an A2MCP service on OKX.AI. Another
4
+ agent describes a product; we return a textured `glb` and a turntable render,
5
+ paid for inside that agent's own workflow.
6
+
7
+ Plan: `docs/plans/2026-07-28-001-feat-agent-3d-product-mockups-plan.md`.
8
+ Research and market measurement: `docs/notes/`.
9
+
10
+ ## Stack
11
+
12
+ Python (`>=3.12`, developed on 3.14), managed with `uv`.
13
+
14
+ | Concern | Choice |
15
+ |---|---|
16
+ | MCP surface | `fastmcp`, stateless with JSON responses |
17
+ | Payments | `x402[evm]`, challenge emitted at the ASGI layer |
18
+ | Persistence | Postgres (`asyncpg`) |
19
+ | Cache, rate limit | Redis |
20
+ | CLI | `typer` + `questionary` + `rich` |
21
+ | Containers | Docker via OrbStack |
22
+ | Load testing | k6 |
23
+
24
+ ## Commands
25
+
26
+ ```bash
27
+ make up # stack: Postgres, Redis, fake generation backend
28
+ make check # ruff + mypy (strict)
29
+ make test # integration suite, randomised order
30
+ make test-seq # sequential, for reproducing one failure
31
+ make load # k6 suites
32
+ make down # tear down, including volumes
33
+
34
+ uv run python -m ondaru.mcp 8000 # the MCP surface + worker (production)
35
+ uv run python -m ondaru.server 58000 # HTTP: asset delivery + load-test routes
36
+ uv run ondaru # interactive: install, order, or check
37
+ uv run ondaru setup-mcp # install into a host agent
38
+ uv run ondaru skills install # install the skill
39
+ uv run ondaru try --url ... # order one mockup end to end, save it
40
+ uv run ondaru credits # what each generation account has left
41
+ uv run ondaru check --url ... # exercise a live service
42
+ ```
43
+
44
+ The integration suite runs against **real** Postgres and Redis. Only the
45
+ generation vendor is faked, by a container speaking the same contract — not an
46
+ in-process stub.
47
+
48
+ ## Invariants
49
+
50
+ Each exists because breaking it produced a concrete failure.
51
+
52
+ **No tool call may occupy the connection beyond 30 seconds.** Measured against
53
+ OKX's own client: a call returning at 29s succeeds, one returning at 35s is
54
+ severed at exactly 30. Progress notifications do **not** extend the window — a
55
+ 60s call emitting progress every 5s still died at 30 with no result, and
56
+ returned an `ok` status with empty content rather than an error. Work that
57
+ cannot finish inside roughly 25 seconds is submitted as a job and retrieved by
58
+ a separate call. Two tests assert the timing directly.
59
+
60
+ **No vendor vocabulary above the generation adapter.** The adapter speaks
61
+ submit, poll, fetch. Vendor field names, status spellings, and staging stop
62
+ there. The public tool contract is what agents install a skill against; a
63
+ change to it after distribution is the expensive kind.
64
+
65
+ **An unrecognised vendor status raises rather than defaulting.** Treating an
66
+ unknown status as "still running" makes the worker poll a task that will never
67
+ resolve.
68
+
69
+ **Never fabricate a deliverable.** A failed job returns a named cause and no
70
+ asset. Partial files are removed on the failure path, so a later fetch cannot
71
+ find a truncated model that looks like success.
72
+
73
+ **The LLM never sets a price.** Pricing is deterministic code reading config,
74
+ and config refuses an asset price at or below the measured generation cost
75
+ floor, or a non-positive spec price — the service will not boot at a
76
+ loss-making price. Asset tier 0.90 against a measured 0.20 cost; spec tier
77
+ 0.02, the entry price behind the highest-volume ASP measured (PixelBrief,
78
+ 21,629 sales). Observed marketplace ceiling: 0.96.
79
+
80
+ **Every tool that produces anything is paid.** `tools/list` is free because
81
+ discovery must be; polling is free because charging per poll bills one job
82
+ repeatedly. Both other tiers sit behind the paywall.
83
+
84
+ **One authorisation buys one job.** Proven the hard way: a captured
85
+ `PAYMENT-SIGNATURE` replayed three times produced four jobs from one 0.60
86
+ payment, each burning ~$0.20 of vendor credit, and the replays carried
87
+ *different* subjects. Signature recovery cannot see this — a replayed header is
88
+ a valid signature. The EIP-3009 nonce is claimed in Postgres before work is
89
+ released, and the primary key is the enforcement; a read-then-write check
90
+ passes serially and loses the race against a replay arriving beside its
91
+ original.
92
+
93
+ **Vendor credit is only ever spent against money that has arrived.**
94
+ `claim_next_queued` passes over any job whose payment has not settled on-chain.
95
+ A verified signature is a promise; generation is a balance leaving the account.
96
+ A failed settlement costs waiting, the other ordering costs money.
97
+
98
+ **A duplicate order is not charged twice.** Its authorisation is written off
99
+ rather than redeemed. Declining to submit *is* the refund — an authorisation
100
+ nobody submits expires costing the payer nothing, and no transaction is needed.
101
+
102
+ **An authorisation that cannot be collected fails its job, loudly.** Expired
103
+ ones are abandoned rather than retried forever, and the job reports
104
+ `payment_not_collected`. Silence is the one outcome a paid API must never
105
+ produce.
106
+
107
+ **Config refuses two mainnet pairings outright.** The fake generation backend
108
+ on mainnet sells placeholder geometry for 0.90, and a localhost or `http://`
109
+ `ONDARU_PUBLIC_BASE_URL` writes an unreachable URL into the job row as the
110
+ delivered model — permanently, since the row is never rewritten. Both defaults
111
+ are right on a laptop and catastrophic in production, and neither fails on its
112
+ own: the service boots, takes payment, and only then produces something
113
+ worthless. Testnet is unguarded, because running the fake backend against a
114
+ fake host is the entire point of it.
115
+
116
+ **A wrong EIP-712 domain is the quietest way to earn nothing.** Signatures
117
+ recover to a different address, every honest payer looks like a forger, and the
118
+ service refuses all revenue while reporting itself healthy. Boot verifies the
119
+ domain against the token's own `DOMAIN_SEPARATOR()` and refuses to start on a
120
+ mismatch. An unreachable RPC only warns — a node being down says nothing about
121
+ the domain, and refusing to start over it trades a quiet failure for a loud
122
+ outage.
123
+
124
+ **Payment checks fail closed; the balance check fails open.** No verifier, no
125
+ nonce claim, or an unverifiable header means refusal. But an unreachable RPC
126
+ during the balance read does *not* refuse a paying caller — a node being down
127
+ is not evidence of an empty wallet, and the settlement gate already protects
128
+ the vendor spend. Know which checks are defences and which are diagnostics.
129
+
130
+ **Invariants that concurrency can break live in the database.** Idempotency is
131
+ a partial unique index; forward-only transitions are a conditional UPDATE.
132
+ Eight concurrent identical orders produce exactly one job; six concurrent
133
+ transitions settle on one winner.
134
+
135
+ **The suite decides where it points; the environment does not.** Generation
136
+ vendor, backend URL, `pay_to`, network, and settlement key are forced in
137
+ `tests/integrations/conftest.py` at import — not defaulted. Deferring to the
138
+ environment meant a sourced `.env` aimed the generation tests at the real Tripo
139
+ API and `pay_to` at a live wallet, and fifty-two tests skipped while the run
140
+ still reported green. Enforce at import: conftest is imported before the test
141
+ modules, and they read these at module level, so a session fixture is too late.
142
+
143
+ **Tests must pass under randomised ordering.** Fixture leakage passes
144
+ sequentially and fails at random. `pytest-randomly` is on by default; use
145
+ `-p no:randomly` only to reproduce a specific failure.
146
+
147
+ ## Test naming
148
+
149
+ Nested classes, three levels: the method, then the case category, then the test.
150
+
151
+ ```python
152
+ class DescribeCreateJob:
153
+ class DescribePositiveCases:
154
+ def test_new_request_creates_queued_job(self): ...
155
+ class DescribeNegativeCases:
156
+ def test_backward_transition_is_rejected(self): ...
157
+ class DescribeEdgeCases:
158
+ def test_concurrent_attempts_settle_on_one_winner(self): ...
159
+ ```
160
+
161
+ When a test enforces an acceptance example from the plan, say so in a comment:
162
+ `Covers AE3.`
163
+
164
+ ## Load testing
165
+
166
+ Constant arrival rate, never ramping VUs — ramping conflates offered load with
167
+ observed latency, and the percentiles end up describing a load nobody chose.
168
+ Thresholds fail the run; verified by running one that cannot be met (k6 exits
169
+ 99). `summaryTrendStats` must include `p(99)` or every summary reports it as
170
+ zero.
171
+
172
+ Measured against the containerised stack:
173
+
174
+ ```
175
+ plan avg 0.9ms med 0.6ms p95 2.2ms p99 3.4ms @ 50 req/s
176
+ order avg 4.0ms med 3.0ms p95 7.8ms p99 16.0ms
177
+ poll avg 1.7ms med 1.5ms p95 3.9ms p99 7.0ms @ 90 req/s
178
+ ```
179
+
180
+ For context the marketplace population sits at p50 543ms, p95 1612ms.
181
+
182
+ With real payments, real settlement, and all three tiers competing for one pool
183
+ (`tests/load/paid_flow.js`, 60s, whole stack in Docker via OrbStack):
184
+
185
+ ```
186
+ avg med p95 p99 throughput
187
+ challenge 2.77ms 0.49ms 5.16ms 75.69ms 60 req/s
188
+ paid 239.43ms 229.47ms 339.93ms 524.27ms 2 req/s
189
+ poll 11.34ms 3.17ms 23.14ms 221.97ms 40 req/s
190
+ 102 req/s total
191
+ ```
192
+
193
+ 120 payments presented, 120 accepted, 0 refused; all 120 redeemed on-chain,
194
+ 2.40 USD₮0 collected, 0 left pending. The paid tier's ~230ms is dominated by
195
+ the X Layer RPC balance read, which is not ours — refusal and polling, which
196
+ are entirely ours, stay under 12ms average.
197
+
198
+ **Send `Accept: application/json, text/event-stream`.** The MCP surface
199
+ negotiates content type and answers 406 before any tool runs. A load test
200
+ missing it measures the rejection: one run reported every paid call refused,
201
+ and the payments had already been claimed.
202
+
203
+ **Layer the rates; do not run every tier flat out.** Free and refused paths run
204
+ at full rate because they cost only CPU. The paid path is sampled at ~2/s
205
+ because every iteration spends real testnet funds and burns a single-use
206
+ authorisation — running it at 50/s drains the payer in seconds and then
207
+ measures an empty wallet.
208
+
209
+ **Load-test payments are real, never stubbed.** `tests/load/paid_flow.js`
210
+ presents genuine EIP-3009 authorisations, pre-signed by
211
+ `tests/load/sign_authorizations.py` against the live token domain, and the
212
+ server verifies each exactly as it verifies a caller's. k6 cannot sign EIP-712,
213
+ and signing per request would measure the signer. One authorisation per
214
+ iteration, never reused — a repeat is the replay the service refuses, and the
215
+ run would measure refusals instead of work.
216
+
217
+ `tests/load/authorizations.json` is gitignored: each entry is a bearer
218
+ instrument that moves real funds when redeemed.
219
+
220
+ ## The install must not cost 200 MB
221
+
222
+ `uvx ondaru setup-mcp` edits a JSON file and copies a skill. Declaring the
223
+ whole service as required dependencies made that a 230 MB, 124-package download
224
+ — a database driver and an EVM stack, to write a config file.
225
+
226
+ Base dependencies are the four the CLI actually needs: `httpx`, `typer`,
227
+ `questionary`, `rich`. Everything else lives behind `ondaru[server]`, which is
228
+ what the Dockerfile installs (`uv sync --extra server`).
229
+
230
+ This only holds because every heavier import in `ondaru.cli` sits inside the
231
+ function that needs it. Move one to module scope and the install quietly
232
+ returns to 230 MB. Measured after the split: **14 MB, 19 packages**, all four
233
+ commands working.
234
+
235
+ ## Two entry points, and they are not symmetric
236
+
237
+ The **agent path is the product**: `uvx ondaru setup-mcp` plus
238
+ `uvx ondaru skills install`, then the host agent orders and pays inside its own
239
+ workflow. `uvx` is the Python equivalent of `npx` — runs without installing.
240
+
241
+ The **CLI is the installer and the demo**, not a second way to buy. Treating it
242
+ as a product would mean it needs a wallet, and a terminal tool holding the
243
+ money is a terminal tool worth stealing. `ondaru try` therefore pays through
244
+ the user's own Agentic Wallet by shelling out to `onchainos payment quote|pay`.
245
+ No private key enters this process.
246
+
247
+ ## Generation vendor — measured, not estimated
248
+
249
+ First real run, 2026-07-28, Tripo `v2.5-20250123`:
250
+
251
+ ```
252
+ credits per textured PBR model 20
253
+ wall clock ~90 seconds
254
+ output valid glTF 2.0 binary, 14.65 MB
255
+ ```
256
+
257
+ At $0.01/credit that is **$0.20 per asset**, inside the $0.19-$0.30 band the
258
+ pricing floor was set from. The 0.60 asset tier keeps roughly $0.40.
259
+
260
+ **90 seconds is the number that justifies the whole architecture.** It is three
261
+ times the client ceiling. A synchronous asset tier would have been severed
262
+ mid-generation on every single order.
263
+
264
+ The model version is **pinned**. Left unset the vendor picks, and today's
265
+ default costs 20 credits while the latest costs 40 for the same job — a default
266
+ change would silently double unit cost.
267
+
268
+ Tripo's free 600 credits are Studio credits that also serve the API; the earlier
269
+ reading that API billing was entirely separate was wrong. 600 credits is 30
270
+ assets.
271
+
272
+ **Capacity comes from several accounts, spent as one.** 30 assets is an
273
+ afternoon for a listed service, so `ONDARU_GENERATION_API_KEYS` takes a
274
+ comma-separated list and the Tripo adapter rotates when a key is spent.
275
+
276
+ Rotation is driven by the vendor's own refusal, never by a tally kept locally —
277
+ a local count drifts the moment anything else spends the same account, and a
278
+ drifted count either strands credit or keeps submitting to an empty key. Only
279
+ submission rotates; a poll stays on the key that started the task, because
280
+ another account has never heard of it.
281
+
282
+ A content-policy refusal must **not** rotate, or one bad prompt burns the whole
283
+ fleet. Both directions are asserted.
284
+
285
+ Keys are masked to their last six characters in every log line. `ondaru
286
+ credits` reads live balances before a demo — discovering an empty account
287
+ mid-order means a paying caller waits on a job that cannot start.
288
+
289
+ ## X Layer payment facts
290
+
291
+ All measured. Re-verify after any vendor or protocol update.
292
+
293
+ - Chain **196**, confirmed via `eth_chainId` against `https://rpc.xlayer.tech`.
294
+ - One settlement token across the entire marketplace — 3,306 service listings,
295
+ zero alternatives: `0x779ded0c9e1022225f8e0630b35a9b54be713736`, `USD₮0`,
296
+ 6 decimals.
297
+ - X Layer is **absent** from the `x402` package's shipped network table, so it
298
+ is registered in `src/ondaru/mcp/payments.py`.
299
+ - The token reverts on `eip712Domain()` and has **no `version()`**, so the
300
+ domain version cannot be read. OKX's payer docs say it defaults to `"2"`; a
301
+ live listed ASP emits `"1"`.
302
+
303
+ Settled arithmetically rather than by preference. `DOMAIN_SEPARATOR()` does
304
+ answer, and the separator is a hash of name, version, chain id, and contract
305
+ — so the domain can be *proved*:
306
+
307
+ ```
308
+ contract 0xd591d9baf744328d9400b923cb02c9474d367d591ca1ab24d8c4068be527599d
309
+ version=1 0xd591d9baf744328d9400b923cb02c9474d367d591ca1ab24d8c4068be527599d match
310
+ version=2 0x27ce12e6ff1ac3cdc0f7d47223876f4f1a4e36f2b29939c257f4ded1be47a27d
311
+ ```
312
+
313
+ `"1"` on mainnet **and** testnet. The service verifies this at boot and
314
+ refuses to start on a mismatch, naming the version that would have worked.
315
+ - The challenge must be a transport-level **HTTP 402** carrying a base64
316
+ `payment-required` header and an **empty body**, and the MCP surface must be
317
+ **stateless with JSON responses**. A JSON-RPC result flagged as an error —
318
+ what the x402 package's MCP wrapper produces — is not recognised.
319
+ - `accepts[].extra` carries exactly `name` and `version`. Extra hints are not
320
+ read by the payer and the working reference ASP does not send them.
321
+
322
+ ## Where things live
323
+
324
+ ```
325
+ src/ondaru/contracts.py shared types — the narrow waist; imports nothing sibling
326
+ src/ondaru/config.py environment-driven settings with guard rails
327
+ src/ondaru/mcp/ MCP tools, payment wiring, ASGI paywall
328
+ src/ondaru/spec/ the specification tier (a plan, never geometry)
329
+ src/ondaru/generation/ the vendor seam and its HTTP implementation
330
+ src/ondaru/worker/ queued -> generating -> rendering -> ready
331
+ src/ondaru/storage/ atomic asset writes and retention
332
+ src/ondaru/store/ Postgres job state, Redis cache and limits
333
+ src/ondaru/server/ asset delivery and load-test routes
334
+ src/ondaru/cli/ installers and the live-service check
335
+ src/ondaru/skill_assets/ the skill shipped to other agents
336
+ fakes/generation-backend/ contract-faithful, failure-injectable vendor
337
+ tests/integrations/ real infrastructure, faked vendor
338
+ tests/load/ k6
339
+ docs/plans/ the implementation plan (U-IDs, R-IDs, AEs)
340
+ references/ cloned repos for study — gitignored, never committed
341
+ ```
342
+
343
+ `references/` is deliberately excluded. Study material lives on disk, not in
344
+ history.
345
+
346
+ ## Deployment
347
+
348
+ `Dockerfile` runs the MCP surface with the worker alongside. Splitting them is
349
+ the right call under real load; one process is one deployment target, and the
350
+ rule that decides eligibility is a live endpoint.
351
+
352
+ Mount a volume at `/app/assets` — a restart otherwise loses everything inside
353
+ its retention window.
354
+
355
+ `ONDARU_PUBLIC_BASE_URL` must be the address callers can actually reach: asset
356
+ URLs are built from it **and written absolute into the job row**. Changing it
357
+ later does not rewrite those rows, so every job inside its 72-hour retention
358
+ window keeps pointing at the old host. The domain is chosen once, before the
359
+ first paid order.
360
+
361
+ The marketplace requires an **`https://`** endpoint, so a plain IP or an
362
+ `http://` host cannot be listed. Registration QA runs at register and update
363
+ time — the endpoint must already be live and correct, and registering first to
364
+ fix afterwards is not a route. Full contract: `docs/going-live.md`.
@@ -0,0 +1,32 @@
1
+ # The deployed service: the MCP surface OKX.AI lists, with the worker running
2
+ # alongside it.
3
+ FROM python:3.12-slim
4
+
5
+ # uv gives the same resolution here as on a developer machine, so a build that
6
+ # passes locally cannot resolve differently in the image.
7
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
8
+
9
+ WORKDIR /app
10
+
11
+ # Dependencies before source, so a code change does not re-resolve the world.
12
+ # `--extra server` because the base install is the CLI only — see pyproject.
13
+ COPY pyproject.toml uv.lock README.md ./
14
+ RUN uv sync --frozen --no-dev --extra server --no-install-project
15
+
16
+ COPY src ./src
17
+ RUN uv sync --frozen --no-dev --extra server
18
+
19
+ ENV PORT=8000
20
+ EXPOSE 8000
21
+
22
+ # Delivered assets live here, and production must mount a volume at this path:
23
+ # a container restart otherwise loses every model inside its 72-hour retention
24
+ # window, and a caller holding a `model_url` gets a 404 for something they paid
25
+ # for.
26
+ #
27
+ # Declared in the platform rather than here. Railway rejects a `VOLUME`
28
+ # instruction outright — "use Railway Volumes" — because it owns the mount, and
29
+ # a Dockerfile that declares one fails the build before it starts.
30
+ RUN mkdir -p /app/assets
31
+
32
+ CMD ["sh", "-c", "uv run python -m ondaru.mcp ${PORT}"]
ondaru-0.1.0/Makefile ADDED
@@ -0,0 +1,31 @@
1
+ .PHONY: up down logs check test test-seq load fresh
2
+
3
+ # Bring the stack up and wait for every health check to pass. Tests that start
4
+ # before Postgres is ready fail in a way that looks like a code bug.
5
+ up:
6
+ docker compose up -d --wait
7
+
8
+ down:
9
+ docker compose down -v
10
+
11
+ logs:
12
+ docker compose logs -f
13
+
14
+ check:
15
+ uv run ruff check .
16
+ uv run mypy src
17
+
18
+ # Randomised order is the default: fixture leakage passes sequentially and
19
+ # fails at random, so the suite has to prove it is order-independent.
20
+ test: up
21
+ uv run pytest tests/integrations
22
+
23
+ # Sequential, for reproducing a specific failure.
24
+ test-seq: up
25
+ uv run pytest tests/integrations -p no:randomly
26
+
27
+ load: up
28
+ k6 run tests/load/spec_tier.js
29
+ k6 run tests/load/order_and_poll.js
30
+
31
+ fresh: down up
ondaru-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: ondaru
3
+ Version: 0.1.0
4
+ Summary: Agent-ordered 3D product mockups, served as an A2MCP service on OKX.AI
5
+ Project-URL: Homepage, https://github.com/Lexirieru/ondaru
6
+ Project-URL: Repository, https://github.com/Lexirieru/ondaru
7
+ License-Expression: MIT
8
+ Keywords: 3d,agent,mcp,okx,x-layer,x402
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: httpx>=0.28.1
15
+ Requires-Dist: questionary>=2.1.1
16
+ Requires-Dist: rich>=15.0.0
17
+ Requires-Dist: typer>=0.27.0
18
+ Provides-Extra: server
19
+ Requires-Dist: asyncpg>=0.30.0; extra == 'server'
20
+ Requires-Dist: eth-abi>=5.2.0; extra == 'server'
21
+ Requires-Dist: fastapi>=0.140.0; extra == 'server'
22
+ Requires-Dist: fastmcp>=3.4.4; extra == 'server'
23
+ Requires-Dist: mcp>=1.28.1; extra == 'server'
24
+ Requires-Dist: redis>=5.2.0; extra == 'server'
25
+ Requires-Dist: uvicorn>=0.51.0; extra == 'server'
26
+ Requires-Dist: x402[evm]>=2.16.0; extra == 'server'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Ondaru
30
+
31
+ **3D product mockups your agent can order.**
32
+
33
+ Another agent describes a product, logo, or character. Ondaru returns a textured
34
+ `glb` and a turntable render — ordered and paid for inside that agent's own
35
+ workflow, with no human in the loop.
36
+
37
+ Listed on [OKX.AI](https://okx.ai) as an A2MCP service.
38
+
39
+ ## Why an agent needs this
40
+
41
+ A language model can describe a mesh. It cannot produce one. That is the whole
42
+ proposition: Ondaru does the part the calling agent genuinely cannot do itself,
43
+ which is what separates a service worth paying for from one that gets tried
44
+ once and abandoned.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install ondaru
50
+ ondaru setup-mcp # register the endpoint with your agent
51
+ ondaru skills install # teach your agent when to call it
52
+ ```
53
+
54
+ Then ask your agent for a product mockup.
55
+
56
+ ## The three tools
57
+
58
+ | Tool | What it does | Cost |
59
+ |---|---|---|
60
+ | `plan_product_mockup` | Dimensions, material, polygon budget, and a firm price — in about a second, because it produces a plan, not geometry | entry tier |
61
+ | `order_product_mockup` | Places the order and returns a job id immediately | asset tier |
62
+ | `check_product_mockup` | Polls the job and collects the asset when ready | free |
63
+
64
+ **No tool waits for generation.** Ordering returns a job id, not a model — you
65
+ poll for the result. That is not a design preference: the marketplace client
66
+ severs any call at 30 seconds, and neither generation nor rendering reliably
67
+ finishes inside that.
68
+
69
+ Surfaces: mug, hoodie, tshirt, phone case, tote, sticker, figurine, packaging.
70
+
71
+ ## Reading the outcome
72
+
73
+ A failed job names its cause and carries **no asset**. There is no placeholder
74
+ and no degraded version — if the `asset` field is absent, nothing was produced.
75
+
76
+ An identical order inside the retention window returns the same job and does
77
+ not charge again, so retrying after a dropped connection is safe.
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ make up # Postgres, Redis, and a fake generation backend under OrbStack
83
+ make check # lint and types
84
+ make test # integration suite against real infrastructure
85
+ make load # k6, reporting avg / median / p95 / p99 and throughput
86
+ ```
87
+
88
+ See `CLAUDE.md` for the invariants and the measured facts behind them, and
89
+ `docs/plans/` for the implementation plan.
90
+
91
+ ## Licence
92
+
93
+ MIT.
ondaru-0.1.0/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Ondaru
2
+
3
+ **3D product mockups your agent can order.**
4
+
5
+ Another agent describes a product, logo, or character. Ondaru returns a textured
6
+ `glb` and a turntable render — ordered and paid for inside that agent's own
7
+ workflow, with no human in the loop.
8
+
9
+ Listed on [OKX.AI](https://okx.ai) as an A2MCP service.
10
+
11
+ ## Why an agent needs this
12
+
13
+ A language model can describe a mesh. It cannot produce one. That is the whole
14
+ proposition: Ondaru does the part the calling agent genuinely cannot do itself,
15
+ which is what separates a service worth paying for from one that gets tried
16
+ once and abandoned.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install ondaru
22
+ ondaru setup-mcp # register the endpoint with your agent
23
+ ondaru skills install # teach your agent when to call it
24
+ ```
25
+
26
+ Then ask your agent for a product mockup.
27
+
28
+ ## The three tools
29
+
30
+ | Tool | What it does | Cost |
31
+ |---|---|---|
32
+ | `plan_product_mockup` | Dimensions, material, polygon budget, and a firm price — in about a second, because it produces a plan, not geometry | entry tier |
33
+ | `order_product_mockup` | Places the order and returns a job id immediately | asset tier |
34
+ | `check_product_mockup` | Polls the job and collects the asset when ready | free |
35
+
36
+ **No tool waits for generation.** Ordering returns a job id, not a model — you
37
+ poll for the result. That is not a design preference: the marketplace client
38
+ severs any call at 30 seconds, and neither generation nor rendering reliably
39
+ finishes inside that.
40
+
41
+ Surfaces: mug, hoodie, tshirt, phone case, tote, sticker, figurine, packaging.
42
+
43
+ ## Reading the outcome
44
+
45
+ A failed job names its cause and carries **no asset**. There is no placeholder
46
+ and no degraded version — if the `asset` field is absent, nothing was produced.
47
+
48
+ An identical order inside the retention window returns the same job and does
49
+ not charge again, so retrying after a dropped connection is safe.
50
+
51
+ ## Development
52
+
53
+ ```bash
54
+ make up # Postgres, Redis, and a fake generation backend under OrbStack
55
+ make check # lint and types
56
+ make test # integration suite against real infrastructure
57
+ make load # k6, reporting avg / median / p95 / p99 and throughput
58
+ ```
59
+
60
+ See `CLAUDE.md` for the invariants and the measured facts behind them, and
61
+ `docs/plans/` for the implementation plan.
62
+
63
+ ## Licence
64
+
65
+ MIT.