Patterns that earn their place
A cookbook of agent flows we've watched succeed in production. None require special API capability — every recipe is reachable with the public endpoints.
Drift correction
Read the mandate, see if the crypto sleeve has drifted past a threshold, propose a trim toward target.
async function rebalanceIfDrifted(ac: AcClient) {
const m = await ac.getMandate();
if (!m.utilization) return;
// 70% of the way through the band ≈ ~28% in a 0.10–0.35 sleeve.
if (m.utilization.cryptoUsedPct < 0.7) return;
const result = await ac.propose({
action: 'sell',
assetSymbol: 'BTC',
assetType: 'crypto',
amountUsd: 8500,
rationale: 'Rebalance crypto sleeve toward 20% target.',
});
if (result.proposed) {
console.log('Pending attestation:', result.serviceRequestId);
} else {
console.warn('Proposal blocked:', result.decision?.violations);
}
}Drawdown response
A signal fires. Probe smaller and smaller sizes via validate until you find one the IPS accepts, then propose with rationale.
async function defensiveTrim(ac: AcClient) {
let amount = 50_000;
let last;
while (amount >= 5_000) {
last = await ac.validate({
action: 'sell',
assetSymbol: 'BTC',
assetType: 'crypto',
amountUsd: amount,
});
if (last.allowed) break;
amount = Math.floor(amount * 0.75);
}
if (!last?.allowed) return;
return ac.propose({
action: 'sell',
assetSymbol: 'BTC',
assetType: 'crypto',
amountUsd: amount,
rationale: 'Drawdown trigger — defensive trim.',
});
}Opportunity sizing
A research signal arrives. Use the IPS’s drawdown headroom as the sizing dial, not the prompt.
async function sizedBuy(ac: AcClient, ticker: string, baseUsd: number) {
const m = await ac.getMandate();
const headroom = m.utilization?.drawdownHeadroomPct ?? 0;
// Cap aggressiveness at 100% even if headroom is huge.
const aggressiveness = Math.min(headroom / 0.15, 1);
const amountUsd = Math.round(baseUsd * aggressiveness);
const v = await ac.validate({
action: 'buy', assetSymbol: ticker,
assetType: 'tradfi', amountUsd,
});
if (!v.allowed) return null;
return ac.propose({
action: 'buy', assetSymbol: ticker,
assetType: 'tradfi', amountUsd,
rationale: `Conviction buy sized to current drawdown headroom (${(headroom * 100).toFixed(1)}%).`,
});
}Watching for attestation outcomes
You proposed. Now you want to know if the user accepted. Two ways:
Webhooks (recommended)
Subscribe to attestation.accepted and attestation.rejected. You get a push within seconds — no polling.
Polling get_audit
Read get_audit every 30–60 seconds for the relevant serviceRequestId. Slower and more rate-limit-noisy, but works in environments where you can’t expose a webhook URL.
async function watchOutcome(ac: AcClient, serviceRequestId: number) {
for (let i = 0; i < 60; i++) {
const audit = await ac.getAudit(100);
const row = audit.find((r) =>
JSON.stringify(r.payload).includes(String(serviceRequestId))
);
if (row) return row;
await new Promise((r) => setTimeout(r, 30_000));
}
}Self-correcting agent
Before retrying a previously-blocked proposal, fetch the audit and inspect what tripped. Shape the next try.
async function retryWithContext(ac: AcClient, baseAmount: number) {
const audit = await ac.getAudit(5);
const lastBlocked = audit.find((r) => !r.allowed);
if (lastBlocked?.violations.some((v) => v.rule === 'crypto_max_allocation')) {
baseAmount = Math.floor(baseAmount * 0.5);
}
return ac.validate({
action: 'buy',
assetSymbol: 'BTC',
assetType: 'crypto',
amountUsd: baseAmount,
});
}Anti-patterns
- Don’t poll
/mandateper trade. Read once at session start. The bands don’t change on the timescales an agent operates on. - Don’t catch and ignore violations. They’re structured — switch on
ruleand reshape, don’t retry blind. - Don’t tell the user the trade executed. A 200 from
proposemeans the policy accepted the proposal. The trade is still pending attestation.