You've already written and locally tested the algorithm. This guide covers the other 50%: building the image with buildx, pushing it to a registry, pinning its checksum, and publishing it on the Pontus-X portal with pontus-x_cli — including the link to the dataset that will be able to run it.
How to navigate
Scroll horizontally, use the ← → arrow keys or the buttons below. There are 14 slides.
Publishing a C2D algorithm is a chain of steps where each one feeds the next. The critical link is the checksum: it ties the physical image to its on-chain description.
Key idea
The portal does not store your code: it stores a reference to the image + its checksum. The C2D node downloads that exact image and runs it in isolation over the dataset.
Recent Docker (buildx is included). Check with docker buildx version.
The own registry (registry.agrospai.udl.cat) or Docker Hub. With push permissions.
To install and run pontus-x_cli and build the scripts with Nautilus.
A .json key file (exportable with export-private-key) to sign the on-chain publication.
With its Dockerfile already tested locally (see the algorithm build guide).
Your own repo where you keep your publishing scripts. AgrospAI uses a private one (agrospai-configs); create yours with the same structure.
Install the CLI
Once: npm install -g pontus-x_cli — or use it from the configs repo, which already lists it as a dependency.
The algorithm doesn't run on your laptop, but on the C2D provider's compute node. If the image architecture doesn't match the node's, the job fails to start (exec format error).
Rule of thumb
If you don't know where it will run, build multi-arch (linux/amd64,linux/arm64). If you do, just target that platform.
Faster, lighter build. Ideal when you know the target node (almost always amd64).
A single tag serves amd64 and arm64 nodes. The registry stores a manifest index pointing to each variant.
buildx can build amd64 from an arm Mac via emulation — slower, but without needing another machine.
docker buildxUnlike docker build, buildx builds and pushes in a single step with --push, and supports several platforms at once with --platform.
docker buildx create --use.registry/namespace/name:tag.Multi-arch needs --push
A multi-platform image can't be loaded into the local Docker (--load); it must go to the registry, which stores the manifest index.
# One-time: create and activate a buildx builder
docker buildx create --name agrospai --use
docker buildx inspect --bootstrap
# --- Option A · single architecture (the node's) ---
docker buildx build \
--platform linux/amd64 \
-t registry.agrospai.udl.cat/library/clip-raster:latest \
--push .
# --- Option B · multi-architecture (portable) ---
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.agrospai.udl.cat/library/clip-raster:latest \
--push .
The trailing . is the build context (the repo root, where the Dockerfile is).
The C2D node must be able to download your image. That means the registry has to be accessible (public, or with credentials the node knows).
registry.agrospai.udl.cat/library/<name>. Full control.docker.io/<user>/<name>. The namespace is your username.docker login before doing --push.Important
The image in the publishing script must be written exactly like the tag you pushed with: same host, same namespace, same name.
# Own registry
docker login registry.agrospai.udl.cat
# Username: ...
# Password: ...
# Docker Hub
docker login # docker.io by default
# After login, the same build with --push uploads it.
# Naming by destination:
# registry.agrospai.udl.cat/library/clip-raster:latest
# docker.io/miempresa/clip-raster:latest
Use a versioned tag (:0.1, :2025-06) in addition to :latest: it lets you re-publish without overwriting previous versions.
checksum: pin the exact imageC2D doesn't trust the tag (:latest can change). Pin the image by its sha256 digest — an immutable hash of the content. That digest is the checksum in the publishing script.
--push already prints the digest when it finishes.If the image changes, the digest changes
Every time you rebuild and push, recompute the checksum and update the asset (re-publish or edit-algo). Otherwise, the node runs the old image.
# Get the digest of the already-pushed image
docker buildx imagetools inspect \
registry.agrospai.udl.cat/library/clip-raster:latest
# Output (abridged):
# Name: registry.agrospai.udl.cat/library/clip-raster:latest
# MediaType: application/vnd.oci.image.index.v1+json
# Digest: sha256:b6ba37cc7a77c936bc03a58b0bbaf9debb55b2
# 5f4b04a097181ee442ea5e50f2
# Manifests:
# linux/amd64 sha256:...
# linux/arm64 sha256:...
Copy the top-level Digest (the index one). That goes, as-is with its sha256: prefix, into the checksum field.
Each asset is a folder inside scripts/ with two files. pontus-x_cli publish takes the path to index.ts, imports it and runs its publish function.
Create your own repo
AgrospAI keeps its scripts in a private repo (agrospai-configs). You create yours replicating this same structure — the repo name doesn't matter; what matters is scripts/<asset>/index.ts.
tu-repo-configs/ # private, yours (same structure)
└── scripts/
└── clip-raster/
├── index.ts # describes and publishes the asset
└── description.md # public description (Markdown)
import { readFileSync } from 'fs';
import {
AssetBuilder, ConsumerParameterBuilder,
FileTypes, ServiceTypes, ServiceBuilder
} from '@deltadao/nautilus'
import { ServiceFileType } from '@deltadao/nautilus'
const publish = async (
folder: string, connection: any,
provider: string, dryRun: boolean
) => {
// 1) metadata 2) algoMetadata 3) service 4) publish
}
AssetBuilderThe AssetBuilder declares what the asset is: type, name, authorship, description, tags and the NFT that represents it on-chain.
description.md.The asset's NFTsetNftData defines the non-fungible token that gives the asset identity and ownership. transferable:false anchors it to your organization.
const assetBuilder = new AssetBuilder();
assetBuilder.setType('algorithm')
.setName('Clip Raster')
.setAuthor('Your Organization')
.setOwner(connection.wallet.address)
.setDescription(
readFileSync(`${folder}/description.md`, 'utf8'))
.addTags(['agrospai', 'remote-sensing',
'sentinel', 'raster'])
.setLicense('MIT')
.setNftData({
name: 'Clip Raster',
symbol: 'AgrospAI-ClipRaster',
templateIndex: 1,
tokenURI: 'https://.../logo.png',
transferable: false
});
checksum: setAlgorithmThe algoMetadata block is the bridge between the portal and your image: it declares which image to run, with which entrypoint, and pinned to which checksum.
sha256 digest from step 3.ocean-execute --base-dir /data.The three must matchimage, tag and checksum must correspond to the same published image. A mismatch here is the #1 cause of jobs that don't start.
const cpb = new ConsumerParameterBuilder();
const refcat = cpb
.setType('text').setName('refcat')
.setLabel('Cadastral Reference')
.setDescription('Cadastral reference code')
.setRequired(true)
.setDefault('25168A010000060000IZ')
.build();
const algoMetadata = {
language: 'python',
version: '0.1',
container: {
entrypoint: 'ocean-execute --base-dir /data',
image: 'registry.agrospai.udl.cat/library/clip-raster',
tag: 'latest',
checksum: 'sha256:b6ba37cc7a77c936bc03a58b0bb...'
},
consumerParameters: [ refcat ]
};
assetBuilder.setAlgorithm(algoMetadata);
The ServiceBuilder defines how the asset is consumed. For a C2D algorithm it's a COMPUTE-type service, with a backing file and a pricing policy.
provider that controls the asset.entrypoint.py)…Auth secrets outside the code
The file can point to a private bucket (minio.agrospai.udl.cat/secrets/…) whose content the node mounts at /data/transformations. That way the algo reads credentials —e.g. the OData login in clip-raster— without embedding them in the image.
const sb = new ServiceBuilder({
serviceType: ServiceTypes.COMPUTE,
fileType: FileTypes.URL });
const urlFile: ServiceFileType<FileTypes> = {
type: 'url',
// the algorithm resource… or a private bucket with
// the auth secrets (mounted at /data/transformations)
url: 'https://minio.agrospai.udl.cat/secrets/<id>/algorithm',
method: 'GET' };
const service = sb
.setServiceEndpoint(provider)
.setTimeout(0)
.addFile(urlFile)
.setPricing(connection.pricingConfig.fixedRateEUROe(1))
.setDatatokenNameAndSymbol(
'AgrospAI Clip Raster', 'AgrospAI-ClipRaster')
.build();
assetBuilder.addService(service);
const asset = assetBuilder.build();
if (!dryRun) await connection.nautilus.publish(asset);
publish with pontus-x_cliConfigure the network in .env, authenticate with your key file, and publish pointing to the --provider and the script folder.
NETWORK (e.g. PONTUSXDEV)..json (your own account/wallet).Your own account
The example .json is UdL's; you use yours (your wallet, exportable with export-private-key). The login determines who signs and who appears as owner — everything else is identical.
NETWORK=PONTUSXDEV
# PRIVATE_KEY is filled in by `login`
# 1) Authenticate with YOUR key file (your account)
pontus-x_cli login TU-CLAVE.json
# 2) Dry run: prints the asset without publishing
pontus-x_cli publish \
--provider https://provider.agrospai.udl.cat \
scripts/clip-raster/index.ts --dry-run
# 3) Publish for real → returns the DID
pontus-x_cli publish \
--provider https://provider.agrospai.udl.cat \
scripts/clip-raster/index.ts
Where does the .json come from? From pontus-x_cli export-private-key, which dumps to a file a wallet you already have (it doesn't create one). Keep it safe: whoever has it can sign as you.
By design, a C2D computation always runs against at least one dataset, and that dataset must explicitly authorize the algorithm with edit-trusted-algos.
edit-trusted-algos overwrites the list: pass all allowed algos at once.Who authorizesedit-trusted-algos is run by the dataset owner. login with that identity first (not the algorithm's, if they differ).
# Authorize your algo on the dataset (as the dataset owner)
pontus-x_cli edit-trusted-algos \
did:op:<DATASET> --algos did:op:<ALGORITHM>
# 1) Publish the empty dataset once
pontus-x_cli publish \
--provider https://provider.agrospai.udl.cat \
scripts/placeholder-dataset/index.ts
# 2) Authorize your algo on the placeholder
pontus-x_cli edit-trusted-algos \
did:op:<PLACEHOLDER> --algos did:op:<ALGORITHM>
The algorithm ignores the placeholder's content and produces its output from its own source. You launch the job with placeholder + algorithm.
docker buildx create --use.--platform (multi-arch if unsure) and --push.sha256 with imagetools inspect.index.ts + description.md.setAlgorithm.login with the owner identity.publish --dry-run, review, and publish for real.edit-trusted-algos on the dataset (or the placeholder).Resources
Reference code clip-raster: github.com/AgrospAI/ocean-algo
Publishing scripts: create your own repo with the structure of agrospai-configs (private) · CLI: github.com/AgrospAI/pontus-x_cli
Questions about the build, the registry or publishing? The AgrospAI team supports you through the integration.
AgrospAI · Universitat de Lleida · Compute-to-Data data space