Every sponsorship deal on Pactsmith carries an editorial verdict from the desk: would this brand read as a credible partner for that collection? For the record side of the product — the ledgered timelines, the audit trail, the splits — the answer lives in a single integer between 0 and 100. This guide walks through how that integer is produced, what the contract asks the model, and why a miss has to come back as null rather than a confidently wrong number.
Where it lives
The scorer is one server function: computeBrandFitScore. It takes a brand name, a collection name, and an optional one-line collection summary, and returns either a number in 0–100 or null. It is called from the deals route handler at scoring time, and the score is persisted on the deal record alongside the splits and the on-chain receipt. You can see the result on every deals board row, on each deal's detail header, and on the collection profile when a deal is the canonical sponsor for that collection.
The rubric the model is told to follow
The scorer hands the LLM a strict system instruction. The whole thing, paraphrased, reads: "You score how well a brand fits a collection for a sponsorship deal. Return STRICT JSON with exactly two fields and nothing else. score: an integer 0–100, where 0 = no plausible fit and 100 = a tailor-made, obvious pairing — shared audience, complementary values, or each side's brand makes the other more credible. justification: one short sentence, twenty-five words or fewer. Do not include any prose outside the JSON. Do not use code fences." The constraints are there for two reasons: deterministic parsing, and a justification that is short enough to print on a card.
The function clamps the response: any non-integer, any value outside 0–100, any JSON the model failed to emit as strict JSON, any network failure — all of those collapse to null. The route handler persists null and skips the badge rather than 500ing or rendering a fabricated score.
Why the call goes through the Polsia AI proxy
The scorer calls generateObject from @/lib/ai/client — the installed ai module. That module routes through the Polsia AI proxy, which means we never hold provider API keys in this repo. There is no OPENAI_API_KEY, no ANTHROPIC_API_KEY, no third-party SDK to upgrade. The scorer asks the proxy for a structured object with a temperature of 0.2, the rubric as the system message, and the brand and collection as the user message — and lets the proxy handle credentials, retries, and transport.
Why null is the right answer on any failure
A sponsorship desk renders its scores visibly: on the deal card, on the deal detail header, and on the collection profile page that lists a deal as a representative sponsor. A wrong number is worse than no number. If the proxy times out, if the model returns prose outside the JSON, if the integer parses to 73.4 instead of a clean 73, if the justification runs to forty words — every one of those collapses to null, the badge renders a quiet dash with aria-label="Brand-fit score not yet computed", and the deal keeps flowing through the platform without inventing a verdict it cannot support.
How a score lands on the deal page
After the route handler writes the score — or the null — onto the deal record, the badge component reads it and tints accordingly: emerald from 70 up, amber from 40 to 69, neutral below that, and a quiet dash for null. The same component renders on the deals board row, on the deal detail header, and on the collection detail page, so a sponsor's bracket is visible anywhere it would change the editor's next move.
What this looks like in practice
Picture a deal between a heritage sneaker brand and a generative-art PFP collection. The scorer reads the collection summary, the brand name, and the rubric; returns, say, an 82 with a justification of "shared design-led audience, both halves reinforce the other's premium positioning". The badge tints emerald on the deal header. Now picture the same brand paired with a derivatives-trading community collection — same inputs, the rubric runs, and the model times out. The route handler persists null, the badge renders a dash, and the editor decides whether to retry later, not whether to trust a guess.
The scoring surface is one file, one contract, and one strict-JSON rubric. Everything around it is plumbing: the proxy, the route handler, the badge component, the persistence layer. The interesting failure mode is not "what score should we show" — the model gets to decide that within its rubric — but "what score can we show without lying", and the answer to that is always null when the rubric didn't return a clean integer. See Pactsmith pricing →