Integration contract · agent-core Brain v2 → UAS
How Brain v2 asks UAS to source a product, and how it reads back a value for every requested spec — per item, keyed by the exact spec name it sent.
Asynchronous. UAS returns 202 with a job_id immediately, sources in the background, and the Brain polls the result. The body is lenient — unknown fields never fail a job, and anything omitted falls back to the Firestore sr_context.
POST /api/sourcing-jobs → 202 { job_id, resultUrl, statusUrl }. Optional Idempotency-Key header dedupes retries.
GET /api/sr/<sr_id>/result → poll until 200 (the ranked shortlist + stats).
GET /job/<job_id>/products → each product with a value for every spec, keyed by name.
required marks the only hard requirement. Everything else is optional and falls back to the Firestore sr_context.
| Field | Type | Description |
|---|---|---|
| sr_id ● | string | number | Sourcing-request id (coerced to string). Keys the persisted result + the resultUrl the Brain polls. required |
| sourcing_request | object | The v2 payload. Fields below are its properties. |
| product | string | Product type. Drives the product-type gate, the seed search keyword, and the fallback query. |
| description | string | Free-text requirements. Grounds spec structuring + search. |
| specs | Spec[] | The requirements to source against and return per item. Shape in the next table. Each spec's name round-trips verbatim to the output. |
| target_price | string | null | Price gate, e.g. "5 USD" / "0.2 CNY". Over-target products are withheld (not deleted) and reported, recoverable via the guide endpoint. null/omit = no gate. |
| moq | number | null | Target order quantity (alias for quantity). A product whose supplier MOQ exceeds it is kept but flagged (buffer_notes) — never dropped. |
| quantity | number | null | Same as moq; if both are sent, quantity wins. |
| customization | boolean | string | A truthy value (or a description) marks the SR custom → suppliers who can't customise are dropped. |
| reference_images | string[] | Product/concept reference image URLs. Used for reverse-image discovery + grounding visual specs (colour/shape/finish). |
| bump_version | boolean | Optional reserved flag; harmless if omitted. |
Spec object · sourcing_request.specs[]| Field | Type | Description |
|---|---|---|
| spec ● | string | The spec name. Returned as spec_name byte-for-byte, so you read values back by it. required |
| value | string | The customer's target / requirement (free prose). Normalized into discrete match values — "white, red, and black" → ["white","red","black"]. |
| reason | string | Why the spec matters (carried as the spec's reasoning seed). |
| source | string | Provenance only (e.g. "customer") — NOT a match hint. UAS derives where to verify (text vs image) itself. |
| priority | enum | important → veto hard filter nice_to_have → rerank preference. Omit → defaults to nice_to_have. |
// POST /api/sourcing-jobs (comments are for docs — send valid JSON) { "sr_id": "3947", "sourcing_request": { "product": "3-color palette cake face paint kit with brush", "description": "3-color face paint kit, white/red/black palette cakes, small brush, soap-and-water washable.", "target_price": "5 USD", // price gate; over-target withheld + reported "moq": 500, // target order quantity (alias for quantity) "customization":"front debossed logo", "reference_images": ["https://your-cdn/ref-1.jpg", "https://your-cdn/ref-2.jpg"], "specs": [ { "spec": "product category", "value": "Halloween 3-color face paint kit", "source": "customer", "priority": "important" }, { "spec": "colors included", "value": "white, red, and black", "source": "customer", "priority": "important" }, { "spec": "brush included", "value": "small brush included", "source": "customer", "priority": "important" }, { "spec": "paint format", "value": "palette cakes", "source": "customer", "priority": "important" }, { "spec": "washability", "value": "soap-and-water washable", "source": "customer", "priority": "important" }, { "spec": "pack format", "value": "plain retail box acceptable", "source": "customer", "priority": "nice_to_have" } ] } }
202 response: { "accepted": true, "job_id": "job_3947_…", "sr_id": "3947", "status": "processing", "statusUrl": "/api/runs/<job_id>", "resultUrl": "/api/sr/3947/result" }
Returns { products: ProcessedProduct[], total_count }. Each product's spec_matching_data carries veto_specs[] (your important specs) and rerank_specs[] (your nice_to_have specs) — every spec you sent, each in this shape:
| Field | Type | Description |
|---|---|---|
| spec_name | string | Your input spec, byte-for-byte. Read values back by this. |
| product_value | string | null | What UAS extracted from the listing (title, structured props, images). null when not found. |
| spec_values | string[] | The customer target (normalized from your value). |
| match_type | enum | match not_match unknown — unknown+null = not stated in the listing (a valid answer, never guessed). |
| reasoning | string | Evidence / why (e.g. "desc image 4: spec table"). |
| priority | enum | important for veto specs, nice_to_have for rerank — mirrors your input. |
"spec_matching_data": { "verdict": "pending", "veto_specs": [ { "spec_name": "colors included", "spec_values": ["white","red","black"], "product_value": "white / red / black", "match_type": "match", "reasoning": "desc image 4: palette", "priority": "important" }, { "spec_name": "washability", "spec_values": ["soap-and-water washable"], "product_value": null, "match_type": "unknown", "reasoning": "not stated in listing", "priority": "important" } ], "rerank_specs": [ /* pack format … */ ] }
Query params: is_shortlisted=true (confirmed only) · include_eliminated=true (append cut products + why) · page / page_size · exclude_spec_matching_data=true · exclude_cleanup_data=true.
Every veto (important) spec confirmed match. The confirmed shortlist.
No veto failed, but ≥1 veto is unknown. Still returned, with all values.
A veto is not_match. Excluded from the shortlist; visible via include_eliminated=true.
spec (in) === spec_name (out), byte-for-byte, for every spec — including the unknown ones. The name is owned by code, never rewritten by a model.important → veto, nice_to_have → rerank. An unconfirmable "important" (e.g. washability a listing doesn't state) leaves the product pending and still returns its extracted value; it's never silently downgraded.unknown. Extraction runs for every matched product.product_value is pulled from the whole listing — title, TMAPI structured props, and the main + description images (spec tables, dimension diagrams, certs, material callouts), not just the title.sr_context; malformed extras never hard-fail a job.Idempotency-Key header; a retried POST with the same key returns the same job_id instead of starting a duplicate run.curl -sS -X POST https://dev.uas.sourcy.ai/api/sourcing-jobs \ -H 'content-type: application/json' \ -H 'idempotency-key: sr3947-run-1' \ -d @request.json # then poll: curl -sS https://dev.uas.sourcy.ai/api/sr/3947/result # then read: curl -sS "https://dev.uas.sourcy.ai/job/<job_id>/products?is_shortlisted=false"