Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A human-facing catalog of small, runnable, frontend-only demonstrations built with live AirnodeHub operations. Each example is backed by a repository README and an agent-ready prompt.

This community workspace is open to everyone and maintained by API3. Its projects focus on the part a normal API example often omits: how an application discovers a source, evaluates it, verifies the signed response locally, and carries the evidence forward.
This community workspace is open to everyone and maintained by API3. It is not an official API3 product. Its projects focus on the part a normal API example often omits: how an application discovers a source, evaluates it, verifies the signed response locally, and carries the evidence forward.

## Architecture boundary

Expand Down Expand Up @@ -54,4 +54,4 @@ Browse the complete [use-case index](use-cases/README.md), inspect the proven [b

## Status

Open to community contributions and maintained by API3.
Open to community contributions and maintained by API3. Not an official API3 product.
2 changes: 1 addition & 1 deletion site/public/llms.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AirnodeHub Community

> An open catalog of runnable, frontend-only AirnodeHub use cases, maintained by API3 and built for community contribution.
> An open catalog of runnable, frontend-only AirnodeHub use cases, maintained by API3 and built for community contribution. It is not an official API3 product.

All demonstrations run in the browser without a proxy, database, account, or private API key. They use live AirnodeHub operations and expose the request, response, signer, verification result, and derived output.

Expand Down
2 changes: 1 addition & 1 deletion site/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function App() {
)}

<footer>
<span>Open to community contributions and maintained by API3.</span>
<span>Open to community contributions and maintained by API3. Not an official API3 product.</span>
</footer>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion site/src/components/CandidateGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function CandidateGrid({
{mode === 'catalog-fallback'
? 'Source search was unavailable, so the demo checked its built-in live source list.'
: mode === 'resolver+catalog'
? 'AirnodeHub matched the question; the demo ranked all three known ETH/USD sources.'
? 'AirnodeHub matched the question. The demo also added its own known ETH/USD sources so the policy can rank them.'
: mode === 'resolver'
? 'AirnodeHub matched the question to live sources.'
: 'No source met the current settings.'}
Expand All @@ -78,6 +78,9 @@ export function CandidateGrid({
<div className="source-copy">
<div>
<strong>{sourceLabel(decision.listing)}</strong>
{candidate.origin === 'demo-catalog' && (
<span className="source-origin">Added by the demo</span>
)}
<span className={`source-status source-status--${decision.status}`}>
{decisionLabel(decision.status)}
</span>
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/ProofPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function ProofPanel({
<div className="result-row">
<div className="price-block">
<span>{verification.valid ? 'ETH / USD' : 'Rejected value · ETH / USD'}</span>
<strong className={tampered ? 'value-flash value-flash--down' : undefined} key={receipt.normalized.value}>
<strong className={tampered ? 'value-flash value-flash--rejected' : undefined} key={receipt.normalized.value}>
<small>$</small>
{receipt.normalized.value.toLocaleString('en-US', {
minimumFractionDigits: 2,
Expand Down
12 changes: 12 additions & 0 deletions site/src/lib/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('price candidate discovery', () => {
const resolvedCoinGecko = {
...PRICE_CATALOG[1],
why: 'Returned by the live resolver.',
origin: 'resolver' as const,
};
const candidates = supplementPriceCandidates([resolvedCoinGecko]);

Expand All @@ -16,4 +17,15 @@ describe('price candidate discovery', () => {
]);
expect(candidates[0]).toBe(resolvedCoinGecko);
});

it('marks supplemented sources as demo additions rather than discoveries', () => {
const resolvedCoinGecko = { ...PRICE_CATALOG[1], origin: 'resolver' as const };
const candidates = supplementPriceCandidates([resolvedCoinGecko]);

expect(candidates.map((candidate) => candidate.origin)).toEqual([
'resolver',
'demo-catalog',
'demo-catalog',
]);
});
});
3 changes: 3 additions & 0 deletions site/src/lib/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const PRICE_CATALOG: Candidate[] = [
example: { name: 'ETH/USD' },
returns: emptyReturns,
payment: null,
origin: 'demo-catalog',
why: 'First-party aggregated ETH/USD data from Nodary.',
},
{
Expand All @@ -36,6 +37,7 @@ export const PRICE_CATALOG: Candidate[] = [
example: { ids: 'ethereum', vs_currencies: 'usd' },
returns: emptyReturns,
payment: null,
origin: 'demo-catalog',
why: 'Current Ethereum price relayed from CoinGecko.',
},
{
Expand All @@ -51,6 +53,7 @@ export const PRICE_CATALOG: Candidate[] = [
example: { assetClass: 'crypto', symbol: 'ETHUSD' },
returns: emptyReturns,
payment: null,
origin: 'demo-catalog',
why: 'Latest ETH/USD trade relayed from TickerLayer.',
},
];
Expand Down
22 changes: 14 additions & 8 deletions site/src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,23 +122,29 @@ async function discover(
);
}

const usesPriceCatalog = supportsCatalogFallback(intent);
const candidates = usesPriceCatalog
? supplementPriceCandidates(resolution.candidates)
: resolution.candidates;
const resolved: Candidate[] = resolution.candidates.map((candidate) => ({
...candidate,
origin: 'resolver',
}));
const candidates = supportsCatalogFallback(intent)
? supplementPriceCandidates(resolved)
: resolved;
const addedCount = candidates.length - resolved.length;

emit(
onTrace,
'discover',
`${candidates.length} candidates discovered`,
usesPriceCatalog
? 'The resolver matched the intent; the demo added its known ETH/USD sources for trust-policy ranking.'
addedCount
? `${resolved.length} discovered, ${addedCount} added by the demo`
: `${resolved.length} candidates discovered`,
addedCount
? 'The resolver returned the discovered sources. The demo added its own known ETH/USD sources so the trust policy can rank them, and marks them separately.'
: 'Parameters, provenance, signer, and payment requirements came from the resolver.',
'success',
);
return {
candidates,
mode: usesPriceCatalog ? 'resolver+catalog' : 'resolver',
mode: addedCount ? 'resolver+catalog' : 'resolver',
attempts: attempt,
};
} catch (error) {
Expand Down
5 changes: 4 additions & 1 deletion site/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface PaymentRequirement {
maxAmountRequired: string;
}

export type CandidateOrigin = 'resolver' | 'demo-catalog';

export interface Candidate {
listing: string;
airnode: string;
Expand All @@ -19,11 +21,12 @@ export interface Candidate {
returns: Record<string, string>;
payment: PaymentRequirement | null;
why: string;
origin: CandidateOrigin;
}

export interface Resolution {
intent: string;
candidates: Candidate[];
candidates: Omit<Candidate, 'origin'>[];
}

export interface Attestation<T = unknown> {
Expand Down
13 changes: 13 additions & 0 deletions site/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,15 @@ main {
font-weight: 600;
}

.source-origin {
padding: 2px 7px;
color: var(--secondary);
background: var(--violet-soft);
border-radius: 5px;
font-size: 11px;
font-weight: 550;
}

.source-status {
padding: 3px 7px;
color: var(--secondary);
Expand Down Expand Up @@ -4653,6 +4662,10 @@ summary:focus-visible {
--value-flash-color: rgba(199, 48, 48, 0.16);
}

.value-flash--rejected {
--value-flash-color: rgba(199, 48, 48, 0.16);
}

@keyframes value-flash {
0% {
background-color: var(--value-flash-color);
Expand Down
Loading