Observed since 20 Oct, 17:40 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
0g-wagmi
Wagmi hooks for 0G Compute Network
A monorepo containing wagmi hooks for interacting with the 0G Compute Network.
0g-wagmi - Core SDK with React hooks for 0G Network integrationexamples - Example React application demonstrating the 0g-wagmi SDK usagecontracts - Smart contracts for Intelligent NFTs (iNFTs) with deployment scriptsnpm install 0g-wagmi wagmi viem @tanstack/react-query ethers @0glabs/0g-serving-broker
| Category | Hook | Description |
|---|---|---|
| AI Inference | use0gChat | Interact with AI models (streaming & non-streaming) |
use0gServices | List available AI services | |
use0gServiceMetadata | Get service metadata for a provider | |
use0gBroker | Get underlying broker instance | |
| Broker Balance | use0gBalance | Query broker balance |
use0gAddFunds | Add funds to broker | |
use0gWithdrawFunds | Withdraw funds from broker | |
use0gInferenceBalance | Query inference balance for provider | |
use0gInferenceAddFunds | Add funds to specific provider | |
| Storage | use0gStorageUpload | Upload files to 0G Storage |
use0gStorageDownload | Download files from 0G Storage | |
use0gStorageFile | Create 0G file objects & compute merkle root | |
| Key-Value | use0gKeyValue | Store and retrieve key-value pairs |
| iNFT | use0gInftMint | Mint intelligent NFTs with AI metadata |
use0gInftList | List user's iNFTs | |
use0gInftMetadata | Fetch and decrypt iNFT metadata | |
| Utilities | useEthersSigner | Convert wagmi client to ethers signer |
use0gChat (Non-streaming)Interact with AI models on the 0G network:
import ReactMarkdown from "react-markdown";
import remarkMath from "remark-math";
import remarkGfm from "remark-gfm";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import "katex/dist/katex.min.css";
import { use0gChat } from '0g-wagmi'
function ChatComponent({ providerAddress }: { providerAddress: string }) {
const { chat, isLoading, error } = use0gChat(providerAddress)
const [question, setQuestion] = useState('')
const [response, setResponse] = useState('')
const handleSubmit = async () => {
try {
const answer = await chat(question)
setResponse(answer)
} catch (err) {
console.error('Chat error:', err)
}
}
return (
<div>
<input
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask a question..."
/>
<button onClick={handleSubmit} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Send'}
</button>
<div>Response:</div>
<div>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]}
>
{response}
</ReactMarkdown>
</div>
{error && <div>Error: {error}</div>}
</div>
)
}
In non-streaming mode, the user only receives the final response once the AI has completed all of its processing. The answer is delivered in full at the end, without showing the AI’s intermediate reasoning or thought process.
use0gChat (Streaming)Interact with AI models on the 0G network with a streaming response by passing a callback function (response, reason) => { ... } in addition to the chat messages:
import ReactMarkdown from "react-markdown";
import remarkMath from "remark-math";
import remarkGfm from "remark-gfm";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import "katex/dist/katex.min.css";
import { use0gChat } from '0g-wagmi'
function ChatComponent({ providerAddress }: { providerAddress: string }) {
const { chat, isLoading, error } = use0gChat(providerAddress)
const [question, setQuestion] = useState('')
const [response, setResponse] = useState('')
const [reason, setReason] = useState('')
const handleSubmit = async () => {
try {
const answer = await chat(question, (answer: string, reason: string) => {
setResponse(answer)
setReason(reason)
})
} catch (err) {
console.error('Chat error:', err)
}
}
return (
<div>
<input
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask a question..."
/>
<button onClick={handleSubmit} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Send'}
</button>
<div>Thinking Reason:</div>
<div>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]}
>
{reason}
</ReactMarkdown>
</div>
<div>Response:</div>
<div>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]}
>
{response}
</ReactMarkdown>
</div>
{error && <div>Error: {error}</div>}
</div>
)
}
In streaming mode, the AI begins sending partial responses as soon as processing starts. Instead of waiting for the full computation to finish, the user can see both the answer and thinking reason unfold in real time.
The chat function also supports multi-turn conversations. Simply pass in your conversation as an array of ChatMessage objects (ChatMessage[]):
import ReactMarkdown from "react-markdown";
import remarkMath from "remark-math";
import remarkGfm from "remark-gfm";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import "katex/dist/katex.min.css";
import { use0gChat, ChatMessage } from '0g-wagmi'
function ChatWithHistory({ providerAddress }: { providerAddress: string }) {
const { chat, isLoading, error } = use0gChat(providerAddress)
const [messages, setMessages] = useState<ChatMessage[]>([
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello, who won the World Cup in 2018?" },
{ role: "assistant", content: "France won the 2018 FIFA World Cup." }
])
const [question, setQuestion] = useState("")
const [response, setResponse] = useState("")
const [reason, setReason] = useState("")
const handleSubmit = async () => {
try {
// Add the new user message to the conversation
const updatedMessages = [...messages, { role: "user", content: question }]
setMessages(updatedMessages)
// Pass the full history to the chat function
await chat(updatedMessages, (answer: string, reason: string) => {
setResponse(answer)
setReason(reason)
})
} catch (err) {
console.error("Chat error:", err)
}
}
return (
<div>
<input
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask a question..."
/>
<button onClick={handleSubmit} disabled={isLoading}>
{isLoading ? "Processing..." : "Send"}
</button>
<div>Thinking Reason:</div>
<div>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]}
>
{reason}
</ReactMarkdown>
</div>
<div>Response:</div>
<div>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]}
>
{response}
</ReactMarkdown>
</div>
{error && <div>Error: {error}</div>}
</div>
)
}
use0gBalanceQuery balance from the 0G broker:
import { use0gBalance } from '0g-wagmi'
function BalanceComponent() {
const { balance, isLoading, refetch } = use0gBalance({
enabled: true,
refetchInterval: 30000, // Refetch every 30 seconds
})
if (isLoading) return <div>Loading...</div>
return (
<div>
<p>Total: {balance ? (Number(balance.totalBalance) / 1e18).toFixed(6) : '0'} OG</p>
<p>Available: {balance ? (Number(balance.availableBalance) / 1e18).toFixed(6) : '0'} OG</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
)
}
use0gAddFundsAdd funds to the 0G broker:
import { use0gAddFunds } from '0g-wagmi'
function AddFundsComponent() {
const { addFunds, isLoading } = use0gAddFunds({
onSuccess: (txHash) => {
console.log('Transaction successful:', txHash)
},
onError: (error) => {
console.error('Transaction failed:', error)
},
})
return (
<button
onClick={() => addFunds('0.1')}
disabled={isLoading}
>
{isLoading ? 'Adding...' : 'Add 0.1 OG'}
</button>
)
}
use0gWithdrawFundsWithdraw funds from the 0G broker:
import { use0gWithdrawFunds } from '0g-wagmi'
function WithdrawComponent() {
const { withdrawFunds, isLoading } = use0gWithdrawFunds({
onSuccess: (txHash) => {
console.log('Withdrawal successful:', txHash)
},
onError: (error) => {
console.error('Withdrawal failed:', error)
},
})
return (
<button
onClick={() => withdrawFunds('0.05')}
disabled={isLoading}
>
{isLoading ? 'Withdrawing...' : 'Withdraw 0.05 OG'}
</button>
)
}
use0gServicesList available AI services on the 0G network:
import { use0gServices } from '0g-wagmi'
function ServicesComponent() {
const { services, isLoading, error } = use0gServices()
if (isLoading) return <div>Loading services...</div>
if (error) return <div>Error loading services</div>
return (
<div>
{services?.map((service, index) => (
<div key={index}>
<h3>{service.model}</h3>
<p>Provider: {service.provider}</p>
<p>Input Price: {service.inputPrice.toString()} per 1M tokens</p>
<p>Output Price: {service.outputPrice.toString()} per 1M tokens</p>
</div>
))}
</div>
)
}
use0gInferenceBalanceQuery inference-specific balance for a provider:
import { use0gInferenceBalance } from '0g-wagmi'
function InferenceBalanceComponent({ providerAddress }: { providerAddress: string }) {
const { balance, isLoading, error, refetch } = use0gInferenceBalance(providerAddress)
if (isLoading) return <div>Loading balance...</div>
if (error) return <div>Error loading balance</div>
return (
<div>
<p>Allocated Credit: {balance ? (Number(balance) / 1e18).toFixed(6) : '0'} OG</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
)
}
use0gInferenceAddFundsAdd funds to a specific inference provider:
import { use0gInferenceAddFunds } from '0g-wagmi'
function AddInferenceFundsComponent({ providerAddress }: { providerAddress: string }) {
const { addFunds, isLoading } = use0gInferenceAddFunds()
const handleAddFunds = async () => {
try {
await addFunds(providerAddress, '0.1')
console.log('Funds added successfully')
} catch (err) {
console.error('Failed to add funds:', err)
}
}
return (
<button onClick={handleAddFunds} disabled={isLoading}>
{isLoading ? 'Adding...' : 'Add 0.1 OG to Provider'}
</button>
)
}
use0gBrokerGet the underlying 0G broker instance:
import { use0gBroker } from '0g-wagmi'
function BrokerComponent() {
const { broker, isLoading, error } = use0gBroker()
if (isLoading) return <div>Initializing broker...</div>
if (error) return <div>Error: {error.message}</div>
if (!broker) return <div>Connect wallet to continue</div>
// Use broker directly for advanced operations
return <div>Broker initialized</div>
}
use0gServiceMetadataGet detailed metadata for a specific service provider:
import { use0gServiceMetadata } from '0g-wagmi'
function ServiceMetadataComponent({ providerAddress }: { providerAddress: string }) {
const { metadata, modelName, endpoint, isLoading, error } = use0gServiceMetadata(providerAddress)
if (isLoading) return <div>Loading service metadata...</div>
if (error) return <div>Error loading metadata</div>
return (
<div>
<h3>Service Details</h3>
<p>Model: {modelName}</p>
<p>Endpoint: {endpoint}</p>
</div>
)
}
use0gStorageUploadUpload files to 0G Storage:
import { use0gStorageUpload } from '0g-wagmi'
function UploadComponent() {
const [file, setFile] = useState<File | null>(null)
const { upload, isLoading, data, error, isSuccess } = use0gStorageUpload({
onSuccess: (result) => {
console.log('File uploaded successfully!')
console.log('Root Hash:', result.rootHash)
console.log('Transaction:', result.txHash)
},
onError: (err) => {
console.error('Upload failed:', err)
},
})
const handleUpload = async () => {
if (!file) return
try {
await upload(file)
} catch (err) {
console.error('Upload error:', err)
}
}
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
<button onClick={handleUpload} disabled={!file || isLoading}>
{isLoading ? 'Uploading...' : 'Upload to 0G Storage'}
</button>
{isSuccess && data && (
<div>
<p>Root Hash: {data.rootHash}</p>
<p>Transaction: {data.txHash}</p>
</div>
)}
{error && <div>Error: {error.message}</div>}
</div>
)
}
use0gStorageDownloadDownload files from 0G Storage:
import { use0gStorageDownload } from '0g-wagmi'
function DownloadComponent() {
const [rootHash, setRootHash] = useState('')
const { download, downloadAndSave, isLoading, data, error } = use0gStorageDownload({
onSuccess: (result) => {
console.log('File downloaded:', result.fileName)
console.log('Size:', result.fileData.byteLength, 'bytes')
},
})
const handleDownload = async () => {
if (!rootHash) return
try {
// Option 1: Download and get the data
const result = await download(rootHash, 'my-file.txt')
// Option 2: Download and automatically save to user's computer
// await downloadAndSave(rootHash, 'my-file.txt')
} catch (err) {
console.error('Download error:', err)
}
}
return (
<div>
<input
type="text"
placeholder="Enter root hash"
value={rootHash}
onChange={(e) => setRootHash(e.target.value)}
/>
<button onClick={handleDownload} disabled={!rootHash || isLoading}>
{isLoading ? 'Downloading...' : 'Download from 0G Storage'}
</button>
{data && (
<div>
<p>File: {data.fileName}</p>
<p>Size: {(data.fileData.byteLength / 1024).toFixed(2)} KB</p>
</div>
)}
{error && <div>Error: {error.message}</div>}
</div>
)
}
use0gStorageFileCreate 0G file objects and compute merkle root hash from browser files:
import { use0gStorageFile } from '0g-wagmi'
function FileInfoComponent() {
const [file, setFile] = useState<File | null>(null)
const { file: zgFile, rootHash, error, isLoading } = use0gStorageFile(file)
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
{isLoading && <div>Computing merkle root...</div>}
{rootHash && (
<div>
<p>File ready for upload</p>
<p>Root Hash: {rootHash}</p>
</div>
)}
{error && <div>Error: {error.message}</div>}
</div>
)
}
use0gKeyValueStore and retrieve key-value pairs on 0G Network:
import { use0gKeyValue } from '0g-wagmi'
function KeyValueComponent() {
const streamId = 'your-stream-id'
const {
setValue,
getValue,
cachedValues,
isSettingValue,
setSuccess
} = use0gKeyValue({
streamId,
onSetSuccess: (result) => {
console.log('Value set successfully!')
console.log('Transaction:', result.txHash)
},
})
// Set a value
const handleSet = async () => {
try {
await setValue('myKey', 'myValue')
} catch (err) {
console.error('Set error:', err)
}
}
// Get a value
const MyValueDisplay = () => {
const { data: value, isLoading } = getValue('myKey')
if (isLoading) return <div>Loading...</div>
return <div>Value: {value || 'Not set'}</div>
}
return (
<div>
<button onClick={handleSet} disabled={isSettingValue}>
{isSettingValue ? 'Setting...' : 'Set Value'}
</button>
<MyValueDisplay />
{/* Access cached values */}
<div>Cached: {cachedValues['myKey'] || 'Not in cache'}</div>
</div>
)
}
use0gInftMintMint intelligent NFTs with encrypted metadata stored on 0G Storage:
import { use0gInftMint, InftMetadata } from '0g-wagmi'
function MintInftComponent() {
const { mint, isLoading, data, error, isSuccess } = use0gInftMint({
onSuccess: (result) => {
console.log('iNFT minted!')
console.log('Token ID:', result.tokenId)
console.log('Metadata Hash:', result.metadataHash)
},
})
const handleMint = async () => {
const metadata: InftMetadata = {
name: 'My AI Agent',
description: 'An intelligent agent with unique capabilities',
aiModel: 'GPT-4',
image: 'https://example.com/image.png',
modelWeights: 'ipfs://weights-hash',
configuration: {
temperature: 0.7,
maxTokens: 2048,
},
attributes: [
{ trait_type: 'Type', value: 'Conversational' },
{ trait_type: 'Intelligence', value: 95 },
],
}
try {
await mint(metadata)
} catch (err) {
console.error('Mint error:', err)
}
}
return (
<div>
<button onClick={handleMint} disabled={isLoading}>
{isLoading ? 'Minting...' : 'Mint iNFT'}
</button>
{isSuccess && data && (
<div>
<p>Token ID: {data.tokenId}</p>
<p>Metadata Hash: {data.metadataHash}</p>
<p>Transaction: {data.txHash}</p>
</div>
)}
{error && <div>Error: {error.message}</div>}
</div>
)
}
use0gInftListList all iNFTs owned by the connected wallet:
import { use0gInftList } from '0g-wagmi'
function MyInftsComponent() {
const { infts, isLoading, refetch } = use0gInftList({
enabled: true,
})
if (isLoading) return <div>Loading iNFTs...</div>
return (
<div>
<h2>My iNFTs ({infts.length})</h2>
<button onClick={() => refetch()}>Refresh</button>
{infts.map((inft) => (
<div key={inft.tokenId}>
<p>Token ID: {inft.tokenId}</p>
<p>Metadata Hash: {inft.metadataHash}</p>
<p>Owner: {inft.owner}</p>
<p>Minted: {new Date(inft.mintedAt).toLocaleDateString()}</p>
</div>
))}
</div>
)
}
use0gInftMetadataFetch and decrypt iNFT metadata from 0G Storage:
import { use0gInftMetadata } from '0g-wagmi'
function InftMetadataComponent({ metadataHash }: { metadataHash: string }) {
const { data: metadata, isLoading, error } = use0gInftMetadata({
metadataHash,
enabled: !!metadataHash,
})
if (isLoading) return <div>Loading metadata...</div>
if (error) return <div>Error: {error.message}</div>
if (!metadata) return null
return (
<div>
<h3>{metadata.name}</h3>
<p>{metadata.description}</p>
{metadata.aiModel && <p>AI Model: {metadata.aiModel}</p>}
{metadata.image && (
<img src={metadata.image} alt={metadata.name} />
)}
{metadata.attributes && (
<div>
<h4>Attributes:</h4>
{metadata.attributes.map((attr, i) => (
<span key={i}>
{attr.trait_type}: {attr.value}
</span>
))}
</div>
)}
</div>
)
}
useEthersSignerConvert wagmi wallet client to ethers.js signer (useful for advanced integrations):
import { useEthersSigner } from '0g-wagmi'
import { ethers } from 'ethers'
function CustomContractComponent() {
const signer = useEthersSigner()
const interactWithContract = async () => {
if (!signer) return
// Use signer with ethers.js contracts
const contract = new ethers.Contract(contractAddress, abi, signer)
const tx = await contract.someFunction()
await tx.wait()
}
return (
<button onClick={interactWithContract} disabled={!signer}>
Interact with Contract
</button>
)
}
The @0glabs/0g-serving-broker package depends on several Node.js server-side modules that are not available in the browser. To prevent build errors, make sure to exclude these modules from the Vite build process by creating app/empty.js and updating your configuration as shown below. You also need to include a Buffer polyfill.
const unsupported = (name) => (..._args) => {
throw new Error(`child_process.${name} is not supported in this environment`);
};
// Functions
export const spawn = unsupported("spawn");
export const exec = unsupported("exec");
export const execFile = unsupported("execFile");
export const fork = unsupported("fork");
export const execSync = unsupported("execSync");
export const spawnSync = unsupported("spawnSync");
export const execFileSync = unsupported("execFileSync");
// Classes / symbols that libraries sometimes reference
export class ChildProcess {}
export const forkOpts = {}; // placeholder sometimes used in examples
// Default export with the same members (helps with `import cp from 'child_process'`)
export default {
spawn,
exec,
execFile,
fork,
execSync,
spawnSync,
execFileSync,
ChildProcess,
forkOpts,
};
export default defineConfig({
...,
resolve: {
alias: {
child_process: "/src/empty.js",
"fs/promises": "/src/empty.js",
fs: "/src/empty.js",
path: "/src/empty.js",
},
},
define: {
global: "globalThis",
},
optimizeDeps: {
include: ["buffer"],
},
});
Install buffer package
This repository includes complete smart contract infrastructure for Intelligent NFTs (iNFTs). See the dedicated documentation:
To deploy the iNFT contract:
cd packages/contracts
pnpm install
cp .env.example .env
# Edit .env with your private key
pnpm deploy
See INFT_QUICKSTART.md for detailed instructions.
Install dependencies:
pnpm install
Build the SDK and examples:
pnpm build
Set up environment variables for the example app:
cp packages/examples/.env.example packages/examples/.env
Edit packages/examples/.env and add your Reown (WalletConnect) project ID:
REOWN_PROJECT_ID=your_project_id_here
Optionally, add your deployed iNFT contract address:
VITE_INFT_CONTRACT_ADDRESS=0x_your_contract_address
Get your project ID from Reown Cloud
Run the example app:
pnpm dev
The app will be available at http://localhost:5173 (or next available port)
The examples package demonstrates all available hooks:
Navigate through the tabs in the demo app to explore each feature.
MIT
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
tsupEnvironment{
"local_name": "tsup",
"alias": false,
"peer_metadata": null
} | ^8.3.6 | development |
viemEnvironment{
"local_name": "viem",
"alias": false,
"peer_metadata": null
} | ^2.37.8 | development |
reactEnvironment{
"local_name": "react",
"alias": false,
"peer_metadata": null
} | ^19.1.0 | development |
wagmiEnvironment{
"local_name": "wagmi",
"alias": false,
"peer_metadata": null
} | ^2.17.5 | development |
ethersEnvironment{
"local_name": "ethers",
"alias": false,
"peer_metadata": null
} | ^6.13.4 | development |
typescriptEnvironment{
"local_name": "typescript",
"alias": false,
"peer_metadata": null
} | ^5.8.3 | development |
@types/reactEnvironment{
"local_name": "@types/react",
"alias": false,
"peer_metadata": null
} | ^19.1.2 | development |
@0glabs/0g-ts-sdkEnvironment{
"local_name": "@0glabs/0g-ts-sdk",
"alias": false,
"peer_metadata": null
} | ^0.3.2 | development |
@tanstack/react-queryEnvironment{
"local_name": "@tanstack/react-query",
"alias": false,
"peer_metadata": null
} | ^5.62.14 | development |
@0glabs/0g-serving-brokerEnvironment{
"local_name": "@0glabs/0g-serving-broker",
"alias": false,
"peer_metadata": null
} | ^0.5.4 | development |
viemEnvironment{
"local_name": "viem",
"alias": false,
"peer_metadata": null
} | >=2.0.0 | peer |
reactEnvironment{
"local_name": "react",
"alias": false,
"peer_metadata": null
} | >=18 | peer |
wagmiEnvironment{
"local_name": "wagmi",
"alias": false,
"peer_metadata": null
} | >=2.0.0 | peer |
ethersEnvironment{
"local_name": "ethers",
"alias": false,
"peer_metadata": null
} | >=6.13.0 | peer |
@0glabs/0g-ts-sdkEnvironment{
"local_name": "@0glabs/0g-ts-sdk",
"alias": false,
"peer_metadata": null
} | ^0.3.2 | peer |
@tanstack/react-queryEnvironment{
"local_name": "@tanstack/react-query",
"alias": false,
"peer_metadata": null
} | >=5.0.0 | peer |
@0glabs/0g-serving-brokerEnvironment{
"local_name": "@0glabs/0g-serving-broker",
"alias": false,
"peer_metadata": null
} | ^0.5.4 | peer |
Source distribution
{
"shasum": "95ced0f19c7d045c70a6124aae881992ccb65d1b",
"tarball": "https://registry.npmjs.org/0g-wagmi/-/0g-wagmi-1.0.2.tgz",
"fileCount": 27,
"integrity": "sha512-0xah6VQbpk7YJRaXpEJdEmH0zGVDha2pzjgtBFqhjxapGiIDE0EfszrLyK9CbXpP6DTGHqJg8lC0QMuU15kdlQ==",
"signatures": [
{
"sig": "MEUCIFTfHnyRha7RAzQbaXsVvwZdo7DLKrQoRqVxyRIUo9nHAiEAtHAic8/NRwIEjvMSCLIzoiKGubZRCM0H7u40alRWrI8=",
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
}
],
"unpackedSize": 248464
}Observed since 20 Oct, 17:40 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:5b6fc791f38d8d9c0e7b21bdc63be7668b537d6d85e8d1fe856bd0ba15f2cf42