PHP SDK for the antd daemon — the gateway to the Autonomi decentralized network.
Source of truth: this SDK is developed in the ant-sdk monorepo.
WithAutonomi/antd-phpis a read-only mirror of that directory, kept in sync by CI so Packagist can index it. Open issues and pull requests againstant-sdk; anything pushed to the mirror is overwritten.
composer require autonomi/antd<?php
require_once 'vendor/autoload.php';
use Autonomi\Antd\AntdClient;
$client = new AntdClient();
// Check daemon health
$health = $client->health();
echo "OK: " . ($health->ok ? 'true' : 'false') . ", Network: {$health->network}\n";
// Store data
$result = $client->dataPutPublic('Hello, Autonomi!');
echo "Stored at {$result->address} (chunks: {$result->chunksStored})\n";
// Retrieve data
$data = $client->dataGetPublic($result->address);
echo "Retrieved: {$data}\n";The antd daemon must be running. Start it with:
ant dev start// Default: http://localhost:8082, 300 second timeout
$client = new AntdClient();
// Custom URL
$client = new AntdClient('http://custom-host:9090');
// Custom timeout (in seconds)
$client = new AntdClient('http://localhost:8082', 30.0);
// Custom Guzzle HTTP client
$client = new AntdClient('http://localhost:8082', 300.0, $myGuzzleClient);| Method | Description |
|---|---|
health() |
Check daemon status |
| Method | Description |
|---|---|
dataPutPublic(string $data, string $paymentMode = 'auto') |
Store public data — returns DataPutPublicResult (DataMap stored on-network) |
dataGetPublic(string $address) |
Retrieve public data by address |
dataPut(string $data, string $paymentMode = 'auto') |
Store encrypted private data — returns DataPutResult (DataMap returned to caller) |
dataGet(string $dataMap) |
Retrieve private data using a caller-held DataMap |
dataCost(string $data, string $paymentMode = 'auto') |
Estimate storage cost — returns UploadCostEstimate with size, chunks, gas, payment mode |
DataPutPublicResult, DataPutResult, FilePutPublicResult, FilePutResult and FinalizeUploadResult (from finalizeUpload()) carry sha256: the plaintext's SHA-256 (hex); requires antd 0.17.0 or later, null on older daemons.
| Method | Description |
|---|---|
chunkPut(string $data) |
Store a raw chunk |
chunkGet(string $address) |
Retrieve a chunk |
| Method | Description |
|---|---|
filePut(string $path, string $paymentMode = 'auto') |
Upload a file privately — returns FilePutResult (DataMap returned to caller) |
fileGet(string $dataMap, string $destPath) |
Download a private file using a caller-held DataMap |
filePutPublic(string $path, string $paymentMode = 'auto') |
Upload a file publicly — returns FilePutPublicResult (DataMap stored on-network) |
fileGetPublic(string $address, string $destPath) |
Download a public file by address |
fileCost(string $path, bool $isPublic, string $paymentMode = 'auto') |
Estimate upload cost — returns UploadCostEstimate with size, chunks, gas, payment mode |
A pointer is a small, owner-signed, mutable reference. Its address is derived from the owner's
key and never changes; what it points at (a chunk, such as public data's data map, or another
pointer) can be changed by the owner, and the owner can hand it over to another pointer for
good. The daemon holds the owner keys, loaded at startup with antd --pointer-keys <file-or-dir>
(files in the format ant pointer keygen writes), so every write names the pointer by its
address ($owner), never by a key. An owner the daemon holds no key for is a BadRequestError.
The pointer methods need antd 0.16.0 or later (the first release with pointers).
Every write pays (about the price of one chunk), and every attempt pays again. The client
never retries pointerCreate, pointerUpdate, pointerTransfer or pointerFinalize, and you
should not retry them automatically either. Reads are free to retry: a CloseGroupShortfallError
on a read usually clears once an update in flight settles.
Counters are strings. A pointer counter is an unsigned 64-bit integer and a final state (a
transfer) sits at 2^64 − 1, past PHP's signed int. The SDK keeps every counter as a validated,
canonical decimal string (digits only, within the u64 range; PointerCounter::FINAL is
"18446744073709551615") and needs neither GMP nor BCMath. A counter that is not a decimal u64
string throws \UnexpectedValueException; it never reads as 0.
| Method | Description |
|---|---|
pointerKeys() |
Addresses of the pointers this daemon holds owner keys for, so can write |
pointerCreate(string $owner, string $target, ?PointerTargetKind $kind = null) |
Create a pointer, paid by the daemon wallet — returns PointerWriteResult (address, stateId, counter). AlreadyExistsError if it exists, refused before paying |
pointerUpdate(string $owner, string $target, ?PointerTargetKind $kind = null) |
Point a pointer at a new target, paid by the daemon wallet; creates it if it does not exist. $kind is PointerTargetKind::Chunk (the daemon's default) or PointerTargetKind::Pointer (a forwarding the owner can still change). PointerFinalError once transferred |
pointerTransfer(string $owner, string $recipient) |
Hand a pointer over for good to the recipient's pointer, which must exist. Irreversible. Returns PointerTransferResult with the close group's finality (null, with finalityError, if that check failed; the transfer still stands) |
pointerGet(string $address) |
Read a pointer — returns PointerRecord (counter, target, kind/kindTag, isFinal, transferredTo, the owner's public key). NotFoundError if there is none |
pointerResolve(string $address) |
Follow a chain of pointers (forwardings and transfers) to the target at its end — returns PointerTarget |
pointerFinality(string $address) |
Ask the whole close group where a pointer stands — returns PointerFinality with a PointerFinalityStatus of Open, Settling, Unconfirmed, Final, Forked or Unknown (a newer daemon's status; rawStatus keeps the string). Check isFinal() before relying on a transfer |
pointerController(string $address) |
Follow transfers to the pointer whose owner now decides — returns PointerController |
pointerPrepare(PointerPrepareRequest $request) |
External signer: the daemon signs PointerPrepareRequest::create(), ::update() or ::transfer() with its key and quotes it — returns PointerPrepareResult (wave-batch payments for payForQuotes()). Needs no daemon wallet. Pass includeSignedQuotes: true to the request factory to also get the full signed quotes in PointerPrepareResult::$signedQuotes (a list of SignedQuoteEntry: quoteHash, base64 quote, optional base64 commitmentSidecar; empty otherwise). Other writes to the pointer throw ConflictError until it is finalized or expires |
pointerFinalize(string $uploadId, array $txHashes) |
External signer: store the prepared write once paid — returns PointerFinalizeResult. A write that falls short after payment throws CloseGroupShortfallError with retryable === true: repeat the same call (same $uploadId) to store it against the same payment |
Every pointer method has an Async variant too.
use Autonomi\Antd\Models\PointerTargetKind;
$owner = $client->pointerKeys()[0];
$first = $client->dataPutPublic('version one')->address;
$second = $client->dataPutPublic('version two')->address;
$client->pointerCreate($owner, $first, PointerTargetKind::Chunk); // pays
$client->pointerUpdate($owner, $second, PointerTargetKind::Chunk); // pays again
$target = $client->pointerResolve($owner)->target; // === $second
echo $client->dataGetPublic($target), "\n"; // "version two"
echo $client->pointerFinality($owner)->rawStatus, "\n"; // "open"
### Manifests
A manifest (ADR-0006) is one `.ant` file, or an `ant://manifest/` link when it is small enough,
that describes a set of files. Each entry has an optional path and size and either an embedded
DataMap or a public DataMap address. The daemon builds and decodes them, so you never handle the
format. An embedded entry carries a private upload's DataMap, the key to that file: anyone with
the manifest or its link can read it. The manifest methods need antd 0.16.0 or later.
**Nothing is uploaded or paid for.** Build a manifest from the DataMaps and addresses your
uploads already returned. Building (with root embedding, the default) and a resolving decode
read from the network, so they can time out; retrying any of these calls is safe.
| Method | Description |
|--------|-------------|
| `buildManifest(array $entries, ?BuildManifestOptions $options = null)` | Build a manifest from `ManifestEntryInput`s — returns `BuiltManifest` (`manifest`: the raw `.ant` bytes; `link`: null when it would exceed 1,500 bytes; `entries`). Each entry sets exactly one of `dataMap` or `address`: both or neither throws `BadRequestError` (400, worded as the daemon words its own) before anything is sent. `BuildManifestOptions` takes `name`, `torrentInfoHashes` and `embedRoots` (default `true`: a shrunk DataMap's root is resolved and embedded when it is at most 64 KiB; `false` embeds DataMaps exactly as given) |
| `decodeManifest(string $manifest, bool $resolve = false)` | Decode raw `.ant` bytes — returns `DecodedManifest` (`source`, `name`, `torrentInfoHashV1`, `torrentInfoHashV2`, `entries`) |
| `decodeManifestLink(string $link, bool $resolve = false)` | Decode an `ant://` manifest link, file link or bare address. A file link or address reads as `source: "file_link"` with one public entry and no path |
Every entry comes back as a `ManifestEntryInfo`: `effectiveName` (the path, or the content
address when there is none), `path`, `size` (the size the creator recorded), `kind`
(`"embedded"` or `"public"`), `contentAddress`, `dataMap` (decode only; null for public
entries), `exactSize`, `error` and `sha256` (the plaintext's SHA-256 in hex, or `null` when the manifest records none; requires antd 0.17.0 or later). With `$resolve` the daemon fetches each entry's DataMap to
report `exactSize`; one entry's failure lands in its `error` instead of failing the call.
**Sizes are ints, and unset is `null`.** `size` and `exactSize` are `?int`: an unset size reads
as `null`, never `0`, and a `null` size on a `ManifestEntryInput` is left out of the request.
The `.ant` bytes are plain PHP strings both ways; the base64 the REST API carries them in never
reaches you. An entry the daemon refuses (bad hex, a path such as `../x`, two entries with one
name) is a `BadRequestError` whose message names each `entries[i]`.
`ManifestEntryInput` takes an optional `sha256` (hex): requires antd 0.17.0 or later. It is sent only when set and non-empty, and passed through as given; the daemon validates it, and a bad value is a `BadRequestError`.
Every manifest method has an `Async` variant too.
```php
use Autonomi\Antd\Models\BuildManifestOptions;
use Autonomi\Antd\Models\ManifestEntryInput;
$public = $client->dataPutPublic('first file')->address; // pays
$private = $client->dataPut('second file')->dataMap; // pays
$built = $client->buildManifest([ // free
new ManifestEntryInput(path: 'notes/one.txt', address: $public),
new ManifestEntryInput(path: 'notes/two.txt', dataMap: $private),
], new BuildManifestOptions(name: 'notes'));
file_put_contents('notes.ant', $built->manifest); // raw .ant bytes
echo $built->link, "\n"; // ant://manifest/... (null if too long)
foreach ($client->decodeManifest($built->manifest, resolve: true)->entries as $entry) {
echo "{$entry->effectiveName}: {$entry->exactSize} bytes\n";
}Every method has an Async variant that returns a GuzzleHttp\Promise\PromiseInterface instead of blocking. This lets you fire off multiple requests concurrently and wait for results when you need them.
use Autonomi\Antd\AntdClient;
$client = new AntdClient();
// Fire an async request — returns immediately
$promise = $client->dataPutPublicAsync('Hello, async Autonomi!');
// Block until the result is available
$result = $promise->wait();
echo "Stored at {$result->address}\n";$client->dataGetPublicAsync($address)
->then(function (string $data) {
echo "Retrieved: {$data}\n";
})
->otherwise(function (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
})
->wait();use GuzzleHttp\Promise\Utils;
// Launch several uploads in parallel
$promises = [
'a' => $client->dataPutPublicAsync('chunk-a'),
'b' => $client->dataPutPublicAsync('chunk-b'),
'c' => $client->dataPutPublicAsync('chunk-c'),
];
// Wait for all to complete — returns ['a' => PutResult, 'b' => PutResult, ...]
$results = Utils::unwrap($promises);
foreach ($results as $key => $result) {
echo "{$key}: stored at {$result->address}\n";
}use GuzzleHttp\Promise\Utils;
// settle() never throws — it returns the state of every promise
$outcomes = Utils::settle($promises)->wait();
foreach ($outcomes as $key => $outcome) {
if ($outcome['state'] === 'fulfilled') {
echo "{$key}: {$outcome['value']->address}\n";
} else {
echo "{$key}: failed — {$outcome['reason']->getMessage()}\n";
}
}All errors extend AntdError (which extends \RuntimeException) and can be caught by type:
use Autonomi\Antd\Errors\NotFoundError;
use Autonomi\Antd\Errors\PaymentError;
try {
$data = $client->dataGetPublic($address);
} catch (NotFoundError $e) {
echo "Data not found on network\n";
} catch (PaymentError $e) {
echo "Insufficient funds\n";
}| Error Type | HTTP Status | When |
|---|---|---|
BadRequestError |
400 | Invalid parameters |
PaymentError |
402 | Insufficient funds |
NotFoundError |
404 | Resource not found |
AlreadyExistsError |
409 | Resource exists |
ForkError |
409 | Version conflict; a pointer whose owner signed two final states with no majority on either (code: "POINTER_FORKED") |
PointerFinalError |
409 | The pointer was transferred or frozen; nothing can change it (code: "POINTER_FINAL") |
ConflictError |
409 | A pointer write while a prepared write to it awaits payment (refused before paying), or an update that lost to a newer state, possibly after paying (code: "CONFLICT") |
TooLargeError |
413 | Payload too large |
InternalError |
500 | Server error |
NetworkError |
502 | Network unreachable |
PartialUploadError |
502 | A finalize stored only part of the upload (code: "PARTIAL_UPLOAD"; extends NetworkError) |
ServiceUnavailableError |
503 | The daemon lacks a dependency, such as a wallet |
CloseGroupShortfallError |
503 | Too few of the close group answered or stored (code: "CLOSE_GROUP_SHORTFALL"); retryable is true only for a pointer finalize that kept the paid state |
The 409s and 503s above are told apart by the error body's code: POINTER_FINAL,
POINTER_FORKED and CONFLICT never read as AlreadyExistsError, and CLOSE_GROUP_SHORTFALL
never reads as ServiceUnavailableError (all three new classes extend AntdError directly).
CLOSE_GROUP_SHORTFALL is not only for pointers: a daemon with the pointer API reports a close
group that fell short on any call this way, where an older daemon sent a 500 (InternalError).
An external-signer finalize (finalizeUpload, finalizeChunkUpload) can fail after the
payment settled: some chunks store, others miss quorum after the daemon's own retries. The
SDK throws PartialUploadError with chunksStored / chunksFailed / totalChunks and two
flags, retryable and retentionKnown (retryable implies retentionKnown). The on-chain
payment persists and the stored chunks stay on the network; what to do next depends on the
flags:
retryable(antd ≥ 0.14.0): the daemon kept the paid attempt under the sameupload_id. Call the same finalize method again with the sameupload_idand tx hashes to store the remainder against the same payment — no re-prepare, no second signature, no double payment. Bound that loop: a persistent failure throws on every call, so cap the attempts and treat achunksFailedthat stops shrinking as stuck.retentionKnown && !retryable: the daemon confirmed nothing was retained (a daemon-wallet upload, or a merkle finalize with deliberately unpaid batches). Re-prepare the same content — already-stored chunks are skipped.!retentionKnown: retention is unknown. The daemon may still hold the paid attempt (it records the resume handle before it returns the error), so stop automatic recovery, keep theupload_idand the original tx hashes, and reconcile before re-preparing or paying again. Never pay again on this signal alone. Daemons older than 0.14.0 never sendretryable, so their partial uploads read as unknown.
use Autonomi\Antd\Errors\PartialUploadError;
try {
$result = $client->finalizeUpload($prep->uploadId, $txHashes);
} catch (PartialUploadError $e) {
if ($e->retryable) {
// Same upload_id, same payment: retry finalizeUpload() with the same
// arguments (bounded — see finalizeWithRetry() in examples/07-external-signer.php).
} elseif ($e->retentionKnown) {
// The daemon confirmed nothing was retained: re-prepare the same content.
} else {
// Retention unknown: keep $prep->uploadId and $txHashes and reconcile
// before re-preparing or paying again. Never pay again on this alone.
}
}The body is read strictly: a count comes only from a non-negative JSON integer (anything else
reads 0), retryable is true only for the JSON boolean true, retentionKnown is true
only when retryable is a JSON boolean (missing, null or any other type reads as unknown),
and a code other than the string "PARTIAL_UPLOAD" keeps the plain NetworkError. A
malformed error body never escapes as a PHP TypeError; the message falls back to the raw body
when error is not a string.
PartialUploadError extends NetworkError, so a pre-existing catch (NetworkError $e) still
catches it; catch the subclass first when you need the counts. The full contract is in
docs/external-signer-flow.md
under "6. Retry a partial store".
See the examples/ directory:
-
01-connect.php— Health check -
02-data.php— Public data storage and retrieval -
03-chunks.php— Raw chunk operations -
04-files.php— File and directory upload/download -
06-private-data.php— Private encrypted data -
07-external-signer.php— External-signer flow with a bounded partial-upload retry -
08-pointers.php— Create, update, resolve and check the finality of a pointer (ANTD_POINTER_OWNER=<address> php examples/08-pointers.php; setANTD_BASE_URLfor a daemon other thanhttp://localhost:8082). Each run pays for its pointer writes. -
09-manifests.php— Build a manifest of two public uploads, print its link, and decode it with resolved sizes (php examples/09-manifests.php; setANTD_BASE_URLfor a daemon other thanhttp://localhost:8082). Each run pays for its two uploads; the manifest calls are free.
Releases are cut from the monorepo: a php-vX.Y.Z tag on ant-sdk is verified,
split, and pushed to the mirror as vX.Y.Z, which Packagist picks up as the
package version. There is no version field in composer.json — Composer
derives it from the mirror's tags.
Dual-licensed under either the MIT or Apache-2.0 license, at your option.