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.
- ondaru-0.1.0/.env.example +39 -0
- ondaru-0.1.0/.gitattributes +2 -0
- ondaru-0.1.0/.gitignore +29 -0
- ondaru-0.1.0/CLAUDE.md +364 -0
- ondaru-0.1.0/Dockerfile +32 -0
- ondaru-0.1.0/Makefile +31 -0
- ondaru-0.1.0/PKG-INFO +93 -0
- ondaru-0.1.0/README.md +65 -0
- ondaru-0.1.0/docker-compose.yml +51 -0
- ondaru-0.1.0/docs/going-live.md +89 -0
- ondaru-0.1.0/docs/notes/2026-07-28-okx-asp-research.md +380 -0
- ondaru-0.1.0/docs/notes/pitch-draft.md +248 -0
- ondaru-0.1.0/docs/notes/sources.md +290 -0
- ondaru-0.1.0/docs/plans/2026-07-28-001-feat-agent-3d-product-mockups-plan.md +518 -0
- ondaru-0.1.0/docs/proof-of-payment.md +142 -0
- ondaru-0.1.0/fakes/generation-backend/Dockerfile +13 -0
- ondaru-0.1.0/fakes/generation-backend/app.py +169 -0
- ondaru-0.1.0/pyproject.toml +94 -0
- ondaru-0.1.0/railway.json +21 -0
- ondaru-0.1.0/src/ondaru/__init__.py +3 -0
- ondaru-0.1.0/src/ondaru/cli/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/cli/main.py +593 -0
- ondaru-0.1.0/src/ondaru/config.py +259 -0
- ondaru-0.1.0/src/ondaru/contracts.py +338 -0
- ondaru-0.1.0/src/ondaru/generation/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/generation/adapter.py +51 -0
- ondaru-0.1.0/src/ondaru/generation/factory.py +27 -0
- ondaru-0.1.0/src/ondaru/generation/tripo.py +274 -0
- ondaru-0.1.0/src/ondaru/generation/vendor.py +154 -0
- ondaru-0.1.0/src/ondaru/mcp/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/mcp/__main__.py +182 -0
- ondaru-0.1.0/src/ondaru/mcp/payments.py +276 -0
- ondaru-0.1.0/src/ondaru/mcp/paywall.py +336 -0
- ondaru-0.1.0/src/ondaru/mcp/server.py +299 -0
- ondaru-0.1.0/src/ondaru/mcp/verification.py +320 -0
- ondaru-0.1.0/src/ondaru/payments/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/payments/balances.py +85 -0
- ondaru-0.1.0/src/ondaru/payments/domain.py +123 -0
- ondaru-0.1.0/src/ondaru/payments/settlement.py +115 -0
- ondaru-0.1.0/src/ondaru/payments/settler.py +343 -0
- ondaru-0.1.0/src/ondaru/server/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/server/__main__.py +43 -0
- ondaru-0.1.0/src/ondaru/server/http_app.py +109 -0
- ondaru-0.1.0/src/ondaru/skill_assets/ondaru.md +70 -0
- ondaru-0.1.0/src/ondaru/spec/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/spec/builder.py +85 -0
- ondaru-0.1.0/src/ondaru/storage/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/storage/assets.py +94 -0
- ondaru-0.1.0/src/ondaru/store/__init__.py +1 -0
- ondaru-0.1.0/src/ondaru/store/postgres.py +412 -0
- ondaru-0.1.0/src/ondaru/store/redis.py +115 -0
- ondaru-0.1.0/src/ondaru/store/schema.sql +81 -0
- ondaru-0.1.0/src/ondaru/worker/pipeline.py +187 -0
- ondaru-0.1.0/tests/__init__.py +0 -0
- ondaru-0.1.0/tests/integrations/__init__.py +0 -0
- ondaru-0.1.0/tests/integrations/_payment_probe_server.py +55 -0
- ondaru-0.1.0/tests/integrations/conftest.py +87 -0
- ondaru-0.1.0/tests/integrations/test_balance_refusal.py +141 -0
- ondaru-0.1.0/tests/integrations/test_business_flows.py +376 -0
- ondaru-0.1.0/tests/integrations/test_cache_and_limits.py +121 -0
- ondaru-0.1.0/tests/integrations/test_cli.py +118 -0
- ondaru-0.1.0/tests/integrations/test_domain_guard.py +160 -0
- ondaru-0.1.0/tests/integrations/test_fake_generation_backend.py +120 -0
- ondaru-0.1.0/tests/integrations/test_generation_adapter.py +208 -0
- ondaru-0.1.0/tests/integrations/test_job_repository.py +198 -0
- ondaru-0.1.0/tests/integrations/test_key_rotation.py +223 -0
- ondaru-0.1.0/tests/integrations/test_mainnet_guards.py +107 -0
- ondaru-0.1.0/tests/integrations/test_mcp_tools.py +327 -0
- ondaru-0.1.0/tests/integrations/test_payment_surface.py +131 -0
- ondaru-0.1.0/tests/integrations/test_paywall_enforcement.py +132 -0
- ondaru-0.1.0/tests/integrations/test_replay_defence.py +297 -0
- ondaru-0.1.0/tests/integrations/test_settlement_gate.py +267 -0
- ondaru-0.1.0/tests/integrations/test_worker_pipeline.py +235 -0
- ondaru-0.1.0/tests/load/order_and_poll.js +107 -0
- ondaru-0.1.0/tests/load/paid_flow.js +141 -0
- ondaru-0.1.0/tests/load/sign_authorizations.py +113 -0
- ondaru-0.1.0/tests/load/spec_tier.js +81 -0
- ondaru-0.1.0/tests/load/thresholds.js +79 -0
- 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
|
ondaru-0.1.0/.gitignore
ADDED
|
@@ -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`.
|
ondaru-0.1.0/Dockerfile
ADDED
|
@@ -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.
|